Skip to content

Commit 576e052

Browse files
Merge branch 'chips' into 'main'
Chips See merge request react-native/react-native-material-components!17
2 parents 46c6b21 + 1e87d04 commit 576e052

30 files changed

+645
-31
lines changed

README.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
Computools react native material components package
44

5+
![Material desgin Figma](https://www.figma.com/design/MY4zoWhMQUMOMIfBGcgSpy/Material-3-Design-Kit-(Community)?node-id=47909-2&p=f&t=Fo0xFwhmWhLMqS8Z-0)
6+
![Material icons design Figma](https://www.figma.com/design/siKJo238QbcA9I2EqYMZ9y/Material-Design-Icons-(Community)?node-id=2402-2207&node-type=canvas&t=xqHD7AwdyHpKWsur-0)
7+
58
## Installation
69

710
1. ```yarn add @computools/react-native-material-components```
@@ -484,6 +487,88 @@ Outlined card is touchable.
484487
![card](https://ik.imagekit.io/Computools/rn-material-components/elevated-card.png?updatedAt=1705074211931)
485488
</details>
486489
</details>
490+
<details><summary>Chips</summary>
491+
<br />
492+
<details><summary>Assist Chip</summary>
493+
<br />
494+
495+
**Properties**
496+
497+
| name | description | type | default |
498+
| ------ | ------ | ------ | ---- |
499+
| label | required | string | - |
500+
| elevated | - | boolean | false |
501+
| LeadingIcon | - | React.FC | - |
502+
| TrailingIcon | - | React.FC | - |
503+
| leadingIconType | COMMON, FAVICON or BRANDED | IconType | COMMON |
504+
| trailingIconType | COMMON, FAVICON or BRANDED | IconType | COMMON |
505+
| leadingIconProps | - | T | - |
506+
| trailingIconProps | - | T | - |
507+
| iconSize | - | number | 18 |
508+
| labelStyle | - | TextStyle | - |
509+
510+
![assist chips](https://ik.imagekit.io/Computools/rn-material-components/assist_chip.png?updatedAt=1734450064327)
511+
512+
</details>
513+
<details><summary>Filter Chip</summary>
514+
<br />
515+
516+
**Properties**
517+
518+
| name | description | type | default |
519+
| ------ | ------ | ------ | ---- |
520+
| label | required | string | - |
521+
| selected | - | boolean | false |
522+
| elevated | - | boolean | false |
523+
| loading | Provide loading to show circle activity indicator instead of leading icon on loading | boolean |false |
524+
| LeadingIcon | - | React.FC | - |
525+
| TrailingIcon | - | React.FC | - |
526+
| leadingIconProps | - | T | - |
527+
| trailingIconProps | - | T | - |
528+
| iconSize | - | number | 18 |
529+
| activityIndicatorSize | - | number | 38 |
530+
531+
![filter chips](https://ik.imagekit.io/Computools/rn-material-components/filter_chip.png?updatedAt=1734450064378)
532+
![filter chip loading state](https://ik.imagekit.io/Computools/rn-material-components/filter_chip_loading_state.gif?updatedAt=1734450161294)
533+
</details>
534+
<details><summary>Input Chip</summary>
535+
<br />
536+
537+
**Properties**
538+
539+
| name | description | type | default |
540+
| ------ | ------ | ------ | ---- |
541+
| label | required | string | - |
542+
| selected | - | boolean | false |
543+
| imageUrl | Provide a url to show leading image. | string | - |
544+
| LeadingIcon | - | React.FC | - |
545+
| TrailingIcon | - | React.FC | - |
546+
| leadingIconProps | - | T | - |
547+
| trailingIconProps | - | T | - |
548+
| iconSize | - | number | 18 |
549+
| hasDefaultTrailingIcon | - | boolean | true |
550+
551+
![input chips](https://ik.imagekit.io/Computools/rn-material-components/input_chip.png?updatedAt=1734450064308)
552+
553+
</details>
554+
<details><summary>Suggestion Chip</summary>
555+
<br />
556+
557+
**Properties**
558+
559+
| name | description | type | default |
560+
| ------ | ------ | ------ | ---- |
561+
| label | required | string | - |
562+
| selected | - | boolean | false |
563+
| elevated | - | boolean | false |
564+
| LeadingIcon | - | React.FC | - |
565+
| TrailingIcon | - | React.FC | - |
566+
| leadingIconProps | - | T | - |
567+
| trailingIconProps | - | T | - |
568+
569+
![suggestion chips](https://ik.imagekit.io/Computools/rn-material-components/suggestion_chip.png?updatedAt=1734450064429)
570+
</details>
571+
</details>
487572
<details><summary>Controls</summary>
488573
<br />
489574

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import React, {useMemo} from 'react';
2+
3+
import {useTheme} from '../../theme/useTheme.hook';
4+
import {type IconProps} from '../../icons/icon-props';
5+
import {getColorStyles, styles} from './assist-chip.styles';
6+
import {BaseChip, type BaseChipProps} from '../base-chip/BaseChip.component';
7+
8+
export enum IconType {
9+
COMMON = 'COMMON',
10+
FAVICON = 'FAVICON',
11+
BRANDED = 'BRANDED',
12+
}
13+
14+
export interface AssistChipProps<T extends IconProps> extends Omit<BaseChipProps, 'leadingIcon' | 'trailingIcon'> {
15+
elevated?: boolean;
16+
LeadingIcon?: React.FC<T>;
17+
TrailingIcon?: React.FC<T>;
18+
leadingIconType?: IconType;
19+
trailingIconType?: IconType;
20+
leadingIconProps?: T;
21+
trailingIconProps?: T;
22+
iconSize?: number;
23+
}
24+
25+
export const AssistChip = <T extends IconProps>({
26+
elevated = false,
27+
LeadingIcon,
28+
TrailingIcon,
29+
leadingIconType = IconType.COMMON,
30+
trailingIconType = IconType.COMMON,
31+
leadingIconProps = {} as T,
32+
trailingIconProps = {} as T,
33+
iconSize = 18,
34+
style,
35+
labelStyle,
36+
disabled = false,
37+
...props
38+
}: AssistChipProps<T>) => {
39+
const theme = useTheme();
40+
const colorStyles = useMemo(() => getColorStyles(elevated, disabled, theme), [disabled, elevated, theme]);
41+
42+
const iconPropsMap: Record<IconType, IconProps> = {
43+
[IconType.COMMON]: {size: iconSize, color: colorStyles.icon.color},
44+
[IconType.FAVICON]: {size: iconSize},
45+
[IconType.BRANDED]: {size: iconSize, style: {opacity: 0.38}},
46+
};
47+
48+
return (
49+
<BaseChip
50+
style={[elevated && !disabled ? styles.elevatedContainer : styles.outlinedContainer, colorStyles.container, style]}
51+
labelStyle={[colorStyles.label, labelStyle]}
52+
leadingIcon={LeadingIcon ? <LeadingIcon {...iconPropsMap[leadingIconType]} {...leadingIconProps} /> : null}
53+
trailingIcon={TrailingIcon ? <TrailingIcon {...iconPropsMap[trailingIconType]} {...trailingIconProps} /> : null}
54+
disabled={disabled}
55+
{...props}
56+
/>
57+
);
58+
};
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import {StyleSheet} from 'react-native';
2+
3+
import {type Theme} from '../../theme/theme.types';
4+
import {convertToRGBA} from '../../utils/convert-to-rgba';
5+
6+
export const styles = StyleSheet.create({
7+
outlinedContainer: {
8+
borderWidth: 1,
9+
},
10+
elevatedContainer: {
11+
shadowOffset: {
12+
width: 0,
13+
height: 1,
14+
},
15+
shadowOpacity: 0.15,
16+
shadowRadius: 3,
17+
elevation: 1,
18+
},
19+
});
20+
21+
export const getColorStyles = (elevated: boolean, disabled: boolean, theme: Theme) => {
22+
const labelDisabledColor = convertToRGBA(theme.surface.text as string, 0.38);
23+
const containerDisabledColor = convertToRGBA(theme.surface.textVariant as string, 0.12);
24+
25+
const [enabledBorderColor, enabledBackgroundColor] = elevated
26+
? ['transparent', theme.surfaceContainer.backgroundLow]
27+
: [theme.outline, 'transparent'];
28+
const [disabledBorderColor, disabledBackgroundColor] = elevated ? ['transparent', containerDisabledColor] : [containerDisabledColor, 'transparent'];
29+
30+
return StyleSheet.create(
31+
disabled
32+
? {
33+
label: {
34+
color: labelDisabledColor,
35+
},
36+
container: {
37+
backgroundColor: disabledBackgroundColor,
38+
borderColor: disabledBorderColor,
39+
},
40+
icon: {
41+
color: labelDisabledColor,
42+
},
43+
}
44+
: {
45+
label: {
46+
color: theme.surface.text,
47+
},
48+
container: {
49+
borderColor: enabledBorderColor,
50+
backgroundColor: enabledBackgroundColor,
51+
},
52+
icon: {
53+
color: theme.primary.background,
54+
},
55+
}
56+
);
57+
};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import React, {type ReactNode} from 'react';
2+
import {TouchableOpacity, Text, type TouchableOpacityProps, type StyleProp, type TextStyle} from 'react-native';
3+
4+
import {styles} from './base-chip.styles';
5+
import {useTypography} from '../../typography/useTypography.component';
6+
7+
export interface BaseChipProps extends TouchableOpacityProps {
8+
label: string;
9+
10+
leadingIcon?: ReactNode;
11+
trailingIcon?: ReactNode;
12+
labelStyle?: StyleProp<TextStyle>;
13+
}
14+
15+
export const BaseChip: React.FC<BaseChipProps> = ({label, leadingIcon, trailingIcon, labelStyle, style, ...props}) => {
16+
const {labelLarge} = useTypography();
17+
18+
return (
19+
<TouchableOpacity style={[styles.container, style]} hitSlop={8} {...props}>
20+
{leadingIcon}
21+
<Text style={[[styles.label, labelLarge, labelStyle]]}>{label}</Text>
22+
{trailingIcon}
23+
</TouchableOpacity>
24+
);
25+
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import {StyleSheet} from 'react-native';
2+
3+
export const styles = StyleSheet.create({
4+
container: {
5+
flexDirection: 'row',
6+
alignItems: 'center',
7+
gap: 4,
8+
9+
borderRadius: 8,
10+
paddingVertical: 6,
11+
paddingHorizontal: 8,
12+
},
13+
label: {
14+
paddingHorizontal: 4,
15+
},
16+
});
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import React, {useMemo} from 'react';
2+
import Animated, {LinearTransition} from 'react-native-reanimated';
3+
4+
import {useTheme} from '../../theme/useTheme.hook';
5+
import {type IconProps} from '../../icons/icon-props';
6+
import {getDynamicStyles, styles} from './filter-chip.styles';
7+
import {BaseChip, type BaseChipProps} from '../base-chip/BaseChip.component';
8+
import {CircularActivityIndicator} from '../../activity-indicators/circular-activity-indicator/CircularActivityIndicator.component';
9+
import {FilterChipLeadingIcon} from './leading-icon/FilterChipLeadingIcon.component';
10+
11+
export interface FilterChipProps<T extends IconProps> extends Omit<BaseChipProps, 'leadingIcon' | 'trailingIcon'> {
12+
elevated?: boolean;
13+
selected?: boolean;
14+
loading?: boolean;
15+
LeadingIcon?: React.FC<T>;
16+
TrailingIcon?: React.FC<T>;
17+
leadingIconProps?: T;
18+
trailingIconProps?: T;
19+
iconSize?: number;
20+
activityIndicatorSize?: number;
21+
}
22+
23+
export const FilterChip = <T extends IconProps>({
24+
elevated = false,
25+
selected = false,
26+
loading = false,
27+
LeadingIcon,
28+
TrailingIcon,
29+
leadingIconProps = {} as T,
30+
trailingIconProps = {} as T,
31+
iconSize = 18,
32+
activityIndicatorSize = 38,
33+
style,
34+
labelStyle,
35+
disabled = false,
36+
...props
37+
}: FilterChipProps<T>) => {
38+
const theme = useTheme();
39+
const dynamicStyles = useMemo(() => getDynamicStyles(selected, elevated, disabled, theme), [disabled, elevated, selected, theme]);
40+
41+
const leadingIcon = loading ? (
42+
<CircularActivityIndicator size={activityIndicatorSize} style={{height: iconSize, width: iconSize}} />
43+
) : (
44+
<FilterChipLeadingIcon
45+
size={iconSize}
46+
selected={selected}
47+
customLeadingIcon={LeadingIcon}
48+
color={dynamicStyles.icon.color}
49+
selectedColor={dynamicStyles.selectedIcon.color}
50+
leadingIconProps={leadingIconProps}
51+
/>
52+
);
53+
54+
return (
55+
<Animated.View layout={LinearTransition}>
56+
<BaseChip
57+
style={[elevated && !disabled ? styles.elevatedContainer : styles.outlinedContainer, dynamicStyles.container, style]}
58+
labelStyle={[dynamicStyles.label, labelStyle]}
59+
leadingIcon={leadingIcon}
60+
trailingIcon={TrailingIcon ? <TrailingIcon size={18} color={dynamicStyles.icon.color} {...trailingIconProps} /> : null}
61+
disabled={disabled}
62+
{...props}
63+
/>
64+
</Animated.View>
65+
);
66+
};
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import {StyleSheet} from 'react-native';
2+
3+
import {type Theme} from '../../theme/theme.types';
4+
import {convertToRGBA} from '../../utils/convert-to-rgba';
5+
6+
export const styles = StyleSheet.create({
7+
outlinedContainer: {
8+
borderWidth: 1,
9+
},
10+
elevatedContainer: {
11+
shadowOffset: {
12+
width: 0,
13+
height: 1,
14+
},
15+
shadowOpacity: 0.15,
16+
shadowRadius: 3,
17+
elevation: 1,
18+
},
19+
});
20+
21+
export const getDynamicStyles = (selected: boolean, elevated: boolean, disabled: boolean, theme: Theme) => {
22+
const labelDisabledColor = convertToRGBA(theme.surface.text as string, 0.38);
23+
const containerDisabledColor = convertToRGBA(theme.surface.textVariant as string, 0.12);
24+
25+
const [disabledUnselectedBackgroundColorByType, disabledUnselectedBorderColorByType] = elevated
26+
? [containerDisabledColor, 'transparent']
27+
: ['transparent', containerDisabledColor];
28+
const [disabledBackgroundColor, disabledBorderColor] = selected
29+
? [containerDisabledColor, 'transparent']
30+
: [disabledUnselectedBackgroundColorByType, disabledUnselectedBorderColorByType];
31+
const enabledBackgroundColorByType = elevated ? theme.surfaceContainer.backgroundLow : 'transparent';
32+
const [enabledBackgroundColor, enabledBorderColor] = selected
33+
? [theme.secondaryContainer.background, theme.secondaryContainer.background]
34+
: [enabledBackgroundColorByType, theme.outline];
35+
36+
return StyleSheet.create(
37+
disabled
38+
? {
39+
label: {
40+
color: labelDisabledColor,
41+
},
42+
container: {
43+
borderColor: disabledBorderColor,
44+
backgroundColor: disabledBackgroundColor,
45+
},
46+
icon: {
47+
color: labelDisabledColor,
48+
},
49+
selectedIcon: {
50+
color: labelDisabledColor,
51+
},
52+
}
53+
: {
54+
label: {
55+
color: theme.surface.text,
56+
},
57+
container: {
58+
borderColor: enabledBorderColor,
59+
borderWidth: 1,
60+
backgroundColor: enabledBackgroundColor,
61+
},
62+
icon: {
63+
color: theme.primary.background,
64+
},
65+
selectedIcon: {
66+
color: theme.secondaryContainer.text,
67+
},
68+
}
69+
);
70+
};

0 commit comments

Comments
 (0)