@proximus/lavender-cards-native 0.0.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,13 @@
1
+ import { ImageSourcePropType } from 'react-native';
2
+ import React from 'react';
3
+ export interface BackgroundImageLinearGradientContainerProps {
4
+ src?: ImageSourcePropType;
5
+ alternativeText?: string;
6
+ contrastHelper?: 'overlay' | 'gradient';
7
+ children?: React.ReactNode;
8
+ backgroundColor?: 'Default' | 'Light';
9
+ inverted?: boolean;
10
+ gradientDirection?: 'horizontal' | 'vertical';
11
+ }
12
+ export declare function BackgroundImageLinearGradientContainer(props: BackgroundImageLinearGradientContainerProps): React.JSX.Element;
13
+ //# sourceMappingURL=BackgroundImageLinearGradientContainer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BackgroundImageLinearGradientContainer.d.ts","sourceRoot":"","sources":["../src/BackgroundImageLinearGradientContainer.tsx"],"names":[],"mappings":"AACA,OAAO,EAAmB,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,MAAM,WAAW,2CAA2C;IAC1D,GAAG,CAAC,EAAE,mBAAmB,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,SAAS,GAAG,UAAU,CAAC;IACxC,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,eAAe,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,iBAAiB,CAAC,EAAE,YAAY,GAAG,UAAU,CAAC;CAC/C;AAkBD,wBAAgB,sCAAsC,CACpD,KAAK,EAAE,2CAA2C,qBA+CnD"}
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ export declare function Content({ content, size, inverted, }: {
3
+ content: string;
4
+ size?: 'M' | 'L';
5
+ inverted?: boolean;
6
+ }): React.JSX.Element;
7
+ //# sourceMappingURL=Content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Content.d.ts","sourceRoot":"","sources":["../src/Content.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,wBAAgB,OAAO,CAAC,EACtB,OAAO,EACP,IAAU,EACV,QAAgB,GACjB,EAAE;IACD,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,GAAG,GAAG,GAAG,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,qBAiBA"}
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import { AccessibilityProps, ImageResizeMode, StyleProp, ViewStyle } from 'react-native';
3
+ import { BackgroundImageLinearGradientContainerProps } from './BackgroundImageLinearGradientContainer';
4
+ export interface PxCardMediaProps {
5
+ mediaPosition?: 'top' | 'left' | 'background';
6
+ }
7
+ export interface ICardProps extends Pick<AccessibilityProps, 'accessibilityLabel' | 'accessibilityHint'>, BackgroundImageLinearGradientContainerProps {
8
+ title: string;
9
+ content?: string;
10
+ backgroundColor?: 'Default' | 'Light';
11
+ backgroundSize?: ImageResizeMode;
12
+ testID?: string;
13
+ children?: React.ReactNode;
14
+ onPress?: () => void;
15
+ containerStyle?: StyleProp<ViewStyle>;
16
+ inverted?: boolean;
17
+ }
18
+ export declare function MediaPart(props: ICardProps & PxCardMediaProps): React.JSX.Element;
19
+ export declare function PxCard(props: ICardProps & PxCardMediaProps): React.JSX.Element;
20
+ //# sourceMappingURL=PxCard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PxCard.d.ts","sourceRoot":"","sources":["../src/PxCard.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EACL,kBAAkB,EAElB,eAAe,EACf,SAAS,EAGT,SAAS,EACV,MAAM,cAAc,CAAC;AAOtB,OAAO,EAEL,2CAA2C,EAC5C,MAAM,0CAA0C,CAAC;AAIlD,MAAM,WAAW,gBAAgB;IAC/B,aAAa,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,YAAY,CAAC;CAC/C;AAED,MAAM,WAAW,UACf,SAAQ,IAAI,CAAC,kBAAkB,EAAE,oBAAoB,GAAG,mBAAmB,CAAC,EAC1E,2CAA2C;IAC7C,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC;IACtC,cAAc,CAAC,EAAE,eAAe,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,cAAc,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,UAAU,GAAG,gBAAgB,qBAwD7D;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,gBAAgB,qBAyD1D"}
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import { ICardProps } from './PxCard';
3
+ import { ImageURISource } from 'react-native';
4
+ export interface PxMessageCardProps extends ICardProps {
5
+ ctaLabel: string;
6
+ ctaAccessibilityLabel?: string;
7
+ ctaAccessibilityHint?: string;
8
+ imageLayout?: 'leading' | 'background';
9
+ src?: ImageURISource;
10
+ closeButtonSize?: 'small' | 'default';
11
+ closeButtonAccessibilityLabel?: string;
12
+ closeButtonAccessibilityHint?: string;
13
+ onClose: () => void;
14
+ tag?: string;
15
+ height?: number;
16
+ }
17
+ export declare function PxMessageCard(props: PxMessageCardProps): React.JSX.Element;
18
+ //# sourceMappingURL=PxMessageCard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PxMessageCard.d.ts","sourceRoot":"","sources":["../src/PxMessageCard.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAS,cAAc,EAAQ,MAAM,cAAc,CAAC;AAM3D,MAAM,WAAW,kBAAmB,SAAQ,UAAU;IACpD,QAAQ,EAAE,MAAM,CAAC;IACjB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAE9B,WAAW,CAAC,EAAE,SAAS,GAAG,YAAY,CAAC;IAEvC,GAAG,CAAC,EAAE,cAAc,CAAC;IAErB,eAAe,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACtC,6BAA6B,CAAC,EAAE,MAAM,CAAC;IACvC,4BAA4B,CAAC,EAAE,MAAM,CAAC;IACtC,OAAO,EAAE,MAAM,IAAI,CAAC;IAEpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,qBA4FtD"}
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ export declare function Title({ title, size, color, inverted, }: {
3
+ title: string;
4
+ size?: 'L' | 'Xl' | '2xl';
5
+ color?: 'Brand' | 'Neutral';
6
+ inverted?: boolean;
7
+ }): React.JSX.Element;
8
+ //# sourceMappingURL=Title.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Title.d.ts","sourceRoot":"","sources":["../src/Title.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,wBAAgB,KAAK,CAAC,EACpB,KAAK,EACL,IAAY,EACZ,KAAe,EACf,QAAgB,GACjB,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,GAAG,GAAG,IAAI,GAAG,KAAK,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,qBAiBA"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=PxCard.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PxCard.spec.d.ts","sourceRoot":"","sources":["../../src/__tests__/PxCard.spec.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=PxMessageCard.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PxMessageCard.spec.d.ts","sourceRoot":"","sources":["../../src/__tests__/PxMessageCard.spec.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,3 @@
1
+ export * from './PxCard';
2
+ export * from './PxMessageCard';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,iBAAiB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@proximus/lavender-cards-native",
3
+ "version": "0.0.1-alpha.0",
4
+ "description": "Card component",
5
+ "license": "MIT",
6
+ "main": "src/index.ts",
7
+ "scripts": {
8
+ "clean": "rm -rf dist",
9
+ "prepublish": "yarn clean && yarn build",
10
+ "build": "npx tsc -p tsconfig.json"
11
+ },
12
+ "peerDependencies": {
13
+ "@proximus/lavender-button-native": "workspace:*",
14
+ "@proximus/lavender-buttonicon-native": "workspace:*",
15
+ "@proximus/lavender-common-native": "workspace:*",
16
+ "@proximus/lavender-styling-native": "workspace:*",
17
+ "@proximus/lavender-tag-native": "workspace:*",
18
+ "@proximus/lavender-texts-native": "workspace:*"
19
+ },
20
+ "devDependencies": {
21
+ "@proximus/lavender-button-native": "0.1.0-alpha.5",
22
+ "@proximus/lavender-buttonicon-native": "0.1.0-alpha.4",
23
+ "@proximus/lavender-common-native": "0.1.0-alpha.4",
24
+ "@proximus/lavender-styling-native": "0.1.0-alpha.3",
25
+ "@proximus/lavender-tag-native": "0.1.0-alpha.1",
26
+ "@proximus/lavender-texts-native": "0.1.0-alpha.4"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "lerna": {
32
+ "command": {
33
+ "publish": {
34
+ "assets": [
35
+ "dist",
36
+ "src",
37
+ "package.json"
38
+ ]
39
+ }
40
+ }
41
+ },
42
+ "gitHead": "8794f25d197ead361646d6e655ce64ba1c7f60c0"
43
+ }
@@ -0,0 +1,80 @@
1
+ import LinearGradient from 'react-native-linear-gradient';
2
+ import { ImageBackground, ImageSourcePropType } from 'react-native';
3
+ import React from 'react';
4
+ import { useTheme } from '@proximus/lavender-styling-native';
5
+
6
+ export interface BackgroundImageLinearGradientContainerProps {
7
+ src?: ImageSourcePropType;
8
+ alternativeText?: string;
9
+ contrastHelper?: 'overlay' | 'gradient';
10
+ children?: React.ReactNode;
11
+ backgroundColor?: 'Default' | 'Light';
12
+ inverted?: boolean;
13
+ gradientDirection?: 'horizontal' | 'vertical';
14
+ }
15
+
16
+ function useStyles(props: BackgroundImageLinearGradientContainerProps) {
17
+ const { theme } = useTheme();
18
+ const { inverted = false, backgroundColor = 'Default' } = props;
19
+ return {
20
+ gradientColors: inverted
21
+ ? ['#252525FF', '#252525DD', '#25252500']
22
+ : ['#FFFFFF', '#FFFFFFDD', '#FFFFFF00'],
23
+ backgroundColor: inverted
24
+ ? theme.t(`ColorBackgroundContainer${backgroundColor}Inverted`)
25
+ : theme.t(`ColorBackgroundContainer${backgroundColor}Default`),
26
+ contrastColors: inverted
27
+ ? ['rgba(0,0,0,0.6)', 'rgba(0,0,0,0.6)']
28
+ : ['rgba(255,255,255,0.7)', 'rgba(255,255,255,0.7)'],
29
+ };
30
+ }
31
+
32
+ export function BackgroundImageLinearGradientContainer(
33
+ props: BackgroundImageLinearGradientContainerProps
34
+ ) {
35
+ const { theme } = useTheme();
36
+ const styles = useStyles(props);
37
+
38
+ const {
39
+ src,
40
+ alternativeText = 'Image',
41
+ contrastHelper = 'none',
42
+ children,
43
+ gradientDirection = 'diagonal',
44
+ } = props;
45
+
46
+ return (
47
+ <ImageBackground
48
+ alt={alternativeText}
49
+ imageStyle={{
50
+ borderRadius: theme.t('RadiusMain').number,
51
+ }}
52
+ style={{
53
+ flex: 1,
54
+ }}
55
+ resizeMode={'cover'}
56
+ source={src}
57
+ >
58
+ <LinearGradient
59
+ colors={[styles.backgroundColor, styles.backgroundColor]}
60
+ {...(contrastHelper === 'gradient' && {
61
+ colors: styles.gradientColors,
62
+ ...(gradientDirection === 'horizontal'
63
+ ? {
64
+ start: { x: 0, y: 1.0 },
65
+ end: { x: 1.0, y: 1.0 },
66
+ }
67
+ : {
68
+ start: { x: 0.0, y: 1.0 },
69
+ end: { x: 0.0, y: 0.0 },
70
+ }),
71
+ })}
72
+ {...(contrastHelper === 'overlay' && {
73
+ colors: styles.contrastColors,
74
+ })}
75
+ >
76
+ {children}
77
+ </LinearGradient>
78
+ </ImageBackground>
79
+ );
80
+ }
@@ -0,0 +1,30 @@
1
+ import { Body } from '@proximus/lavender-texts-native';
2
+ import React from 'react';
3
+ import { useTheme } from '@proximus/lavender-styling-native';
4
+
5
+ export function Content({
6
+ content,
7
+ size = 'M',
8
+ inverted = false,
9
+ }: {
10
+ content: string;
11
+ size?: 'M' | 'L';
12
+ inverted?: boolean;
13
+ }) {
14
+ const { theme } = useTheme();
15
+
16
+ return (
17
+ <Body
18
+ style={{
19
+ color: inverted
20
+ ? theme.t(`ColorTextDimmedInverted`)
21
+ : theme.t(`ColorTextNeutralDefault`),
22
+ fontSize: theme.t(`TextSizeBody${size}Mobile`).number,
23
+ }}
24
+ ellipsizeMode={'tail'}
25
+ numberOfLines={3}
26
+ >
27
+ {content}
28
+ </Body>
29
+ );
30
+ }
package/src/PxCard.tsx ADDED
@@ -0,0 +1,157 @@
1
+ import React from 'react';
2
+ import {
3
+ AccessibilityProps,
4
+ Image,
5
+ ImageResizeMode,
6
+ StyleProp,
7
+ TouchableOpacity,
8
+ View,
9
+ ViewStyle,
10
+ } from 'react-native';
11
+ import { useTheme } from '@proximus/lavender-styling-native';
12
+ import {
13
+ a11y,
14
+ e2eID,
15
+ useImageAspectRatio,
16
+ } from '@proximus/lavender-common-native';
17
+ import {
18
+ BackgroundImageLinearGradientContainer,
19
+ BackgroundImageLinearGradientContainerProps,
20
+ } from './BackgroundImageLinearGradientContainer';
21
+ import { Title } from './Title';
22
+ import { Content } from './Content';
23
+
24
+ export interface PxCardMediaProps {
25
+ mediaPosition?: 'top' | 'left' | 'background';
26
+ }
27
+
28
+ export interface ICardProps
29
+ extends Pick<AccessibilityProps, 'accessibilityLabel' | 'accessibilityHint'>,
30
+ BackgroundImageLinearGradientContainerProps {
31
+ title: string;
32
+ content?: string;
33
+ backgroundColor?: 'Default' | 'Light';
34
+ backgroundSize?: ImageResizeMode;
35
+ testID?: string;
36
+ children?: React.ReactNode;
37
+ onPress?: () => void;
38
+ containerStyle?: StyleProp<ViewStyle>;
39
+ inverted?: boolean;
40
+ }
41
+
42
+ export function MediaPart(props: ICardProps & PxCardMediaProps) {
43
+ const { theme } = useTheme();
44
+
45
+ const {
46
+ src,
47
+ backgroundSize = 'cover',
48
+ mediaPosition = 'top',
49
+ alternativeText,
50
+ } = props;
51
+
52
+ const aspectRatio = useImageAspectRatio(src);
53
+ const [wrapperHeight, setWrapperHeight] = React.useState<number | undefined>(
54
+ undefined
55
+ );
56
+
57
+ /*
58
+ If we don’t set width (or height), RN needs one concrete dimension for image. Local assets
59
+ have an intrinsic size, so they render at that size (can be wider than the parent).
60
+ Remote images often don’t have an intrinsic size at first and end up constrained by
61
+ the parent, so they look correct.
62
+
63
+ We then wrap the image in a View to be able to set the aspect ratio and
64
+ make sure Image takes the full width of the card when mediaPosition is 'top'.
65
+ */
66
+ return (
67
+ <View
68
+ style={mediaPosition === 'top' && { width: '100%', aspectRatio }}
69
+ onLayout={(event) => {
70
+ // when media position is left, we need to set the height of the image to match the height of the wrapper
71
+ // which itself have the height of the taller's neighbor element
72
+ const { height } = event.nativeEvent.layout;
73
+ setWrapperHeight(height);
74
+ }}
75
+ >
76
+ <Image
77
+ alt={alternativeText}
78
+ style={{
79
+ ...(mediaPosition === 'top' && {
80
+ width: '100%',
81
+ height: '100%',
82
+ aspectRatio,
83
+ borderTopRightRadius: theme.t('RadiusMain').number,
84
+ borderTopLeftRadius: theme.t('RadiusMain').number,
85
+ }),
86
+ ...(mediaPosition === 'left' && {
87
+ width: 120,
88
+ height: wrapperHeight,
89
+ borderBottomLeftRadius: theme.t('RadiusMain').number,
90
+ borderTopLeftRadius: theme.t('RadiusMain').number,
91
+ }),
92
+ }}
93
+ source={src}
94
+ resizeMode={backgroundSize}
95
+ />
96
+ </View>
97
+ );
98
+ }
99
+
100
+ export function PxCard(props: ICardProps & PxCardMediaProps) {
101
+ const {
102
+ title,
103
+ content,
104
+ children,
105
+ mediaPosition,
106
+ containerStyle,
107
+ accessibilityLabel,
108
+ accessibilityHint,
109
+ testID,
110
+ onPress,
111
+ src,
112
+ } = props;
113
+
114
+ const flexDirection = mediaPosition === 'left' ? 'row' : 'column';
115
+
116
+ const { theme } = useTheme();
117
+
118
+ return (
119
+ <TouchableOpacity onPress={onPress}>
120
+ <View
121
+ style={[
122
+ containerStyle,
123
+ {
124
+ overflow: 'hidden',
125
+ flexDirection,
126
+ borderRadius: theme.t('RadiusMain').number,
127
+ },
128
+ ]}
129
+ {...e2eID(testID)}
130
+ {...a11y({
131
+ accessibilityLabel,
132
+ accessibilityHint,
133
+ role: 'button',
134
+ })}
135
+ >
136
+ {mediaPosition !== 'background' && <MediaPart {...props} />}
137
+
138
+ <BackgroundImageLinearGradientContainer
139
+ {...props}
140
+ src={mediaPosition !== 'background' ? undefined : src}
141
+ >
142
+ {/*wrapping linear gradient with content is needed as styling directly it is buggy*/}
143
+ <View
144
+ style={{
145
+ padding: theme.t(`PaddingMMobile`).number,
146
+ gap: theme.t(`PaddingSMobile`).number,
147
+ }}
148
+ >
149
+ <Title title={title} />
150
+ {content && <Content content={content} />}
151
+ {children}
152
+ </View>
153
+ </BackgroundImageLinearGradientContainer>
154
+ </View>
155
+ </TouchableOpacity>
156
+ );
157
+ }
@@ -0,0 +1,174 @@
1
+ import { a11y, e2eID, PxSpacer } from '@proximus/lavender-common-native';
2
+ import { Title } from './Title';
3
+ import { Content } from './Content';
4
+ import { BackgroundImageLinearGradientContainer } from './BackgroundImageLinearGradientContainer';
5
+ import React from 'react';
6
+ import { ICardProps } from './PxCard';
7
+ import { Image, ImageURISource, View } from 'react-native';
8
+ import { useTheme } from '@proximus/lavender-styling-native';
9
+ import { PxButton } from '@proximus/lavender-button-native';
10
+ import { PxButtonIcon } from '@proximus/lavender-buttonicon-native';
11
+ import { PxTag } from '@proximus/lavender-tag-native';
12
+
13
+ export interface PxMessageCardProps extends ICardProps {
14
+ ctaLabel: string;
15
+ ctaAccessibilityLabel?: string;
16
+ ctaAccessibilityHint?: string;
17
+
18
+ imageLayout?: 'leading' | 'background';
19
+
20
+ src?: ImageURISource;
21
+
22
+ closeButtonSize?: 'small' | 'default';
23
+ closeButtonAccessibilityLabel?: string;
24
+ closeButtonAccessibilityHint?: string;
25
+ onClose: () => void;
26
+
27
+ tag?: string;
28
+ height?: number;
29
+ }
30
+
31
+ export function PxMessageCard(props: PxMessageCardProps) {
32
+ const {
33
+ children,
34
+ containerStyle,
35
+ accessibilityLabel,
36
+ accessibilityHint,
37
+ testID,
38
+ ctaLabel,
39
+ ctaAccessibilityLabel,
40
+ ctaAccessibilityHint,
41
+ onPress,
42
+ imageLayout = 'leading',
43
+ src,
44
+ onClose,
45
+ closeButtonSize = 'small',
46
+ closeButtonAccessibilityHint,
47
+ closeButtonAccessibilityLabel,
48
+ inverted,
49
+ tag,
50
+ backgroundColor = 'Light',
51
+ gradientDirection = 'vertical',
52
+ height,
53
+ } = props;
54
+
55
+ const { theme } = useTheme();
56
+
57
+ return (
58
+ <View
59
+ style={[
60
+ containerStyle,
61
+ {
62
+ overflow: 'hidden',
63
+ borderRadius: theme.t('RadiusMain').number,
64
+ height,
65
+ },
66
+ ]}
67
+ {...e2eID(testID)}
68
+ {...a11y({
69
+ accessibilityLabel,
70
+ accessibilityHint,
71
+ role: 'button',
72
+ })}
73
+ >
74
+ <BackgroundImageLinearGradientContainer
75
+ {...props}
76
+ backgroundColor={backgroundColor}
77
+ src={imageLayout === 'background' ? src : undefined}
78
+ gradientDirection={gradientDirection}
79
+ >
80
+ <View
81
+ style={{
82
+ gap: theme.t('SpacingDefaultMobile').number,
83
+ padding: theme.t('PaddingSMobile').number,
84
+ height: '100%',
85
+ }}
86
+ >
87
+ {tag && <TagSection {...props} />}
88
+ {height && <PxSpacer />}
89
+ <TitleSection {...props} />
90
+ {children}
91
+ <PxButton
92
+ title={ctaLabel}
93
+ onPress={onPress}
94
+ type="secondary"
95
+ accessibilityLabel={ctaAccessibilityLabel || ctaLabel}
96
+ accessibilityHint={ctaAccessibilityHint}
97
+ testID={`${testID ? testID : 'message-card'}-cta-button`}
98
+ inverted={inverted}
99
+ />
100
+ <PxButtonIcon
101
+ size={closeButtonSize}
102
+ iconName={'cross'}
103
+ variant={'secondary'}
104
+ onPress={onClose}
105
+ wrapperStyle={{
106
+ position: 'absolute',
107
+ top: theme.t('SpacingSMobile').number,
108
+ right: theme.t('SpacingSMobile').number,
109
+ }}
110
+ inverted={inverted}
111
+ {...e2eID(
112
+ testID ? `${testID}-close-button` : 'message-card-close-button'
113
+ )}
114
+ {...a11y({
115
+ accessibilityLabel: `${closeButtonAccessibilityLabel || 'Close'}`,
116
+ accessibilityHint: closeButtonAccessibilityHint,
117
+ })}
118
+ />
119
+ </View>
120
+ </BackgroundImageLinearGradientContainer>
121
+ </View>
122
+ );
123
+ }
124
+
125
+ function TitleSection(props: PxMessageCardProps) {
126
+ const { theme } = useTheme();
127
+ const { imageLayout, src, inverted, content, title } = props;
128
+
129
+ return (
130
+ <View
131
+ style={{
132
+ gap: theme.t('SpacingDefaultMobile').number,
133
+ flexDirection: 'row',
134
+ }}
135
+ >
136
+ {imageLayout === 'leading' && (
137
+ <Image
138
+ source={src}
139
+ style={{
140
+ width: theme.t('SizeAssetsM').number,
141
+ aspectRatio: 1,
142
+ }}
143
+ />
144
+ )}
145
+ <View
146
+ style={{
147
+ gap: theme.t('SpacingXsMobile').number,
148
+ flexDirection: 'column',
149
+ }}
150
+ >
151
+ <Title inverted={inverted} title={title} color={'Neutral'} size={'L'} />
152
+ {content && (
153
+ <Content inverted={inverted} content={content} size={'M'} />
154
+ )}
155
+ </View>
156
+ </View>
157
+ );
158
+ }
159
+
160
+ function TagSection(props: PxMessageCardProps) {
161
+ const { theme } = useTheme();
162
+ const { tag, inverted } = props;
163
+
164
+ return (
165
+ <View
166
+ style={{
167
+ gap: theme.t('PaddingMMobile').number,
168
+ alignItems: 'flex-start',
169
+ }}
170
+ >
171
+ <PxTag label={tag!} inverted={inverted} />
172
+ </View>
173
+ );
174
+ }
package/src/Title.tsx ADDED
@@ -0,0 +1,32 @@
1
+ import { Header } from '@proximus/lavender-texts-native';
2
+ import React from 'react';
3
+ import { useTheme } from '@proximus/lavender-styling-native';
4
+
5
+ export function Title({
6
+ title,
7
+ size = '2xl',
8
+ color = 'Brand',
9
+ inverted = false,
10
+ }: {
11
+ title: string;
12
+ size?: 'L' | 'Xl' | '2xl';
13
+ color?: 'Brand' | 'Neutral';
14
+ inverted?: boolean;
15
+ }) {
16
+ const { theme } = useTheme();
17
+
18
+ const invertedSuffix = inverted ? 'Inverted' : 'Default';
19
+
20
+ return (
21
+ <Header
22
+ ellipsizeMode={'tail'}
23
+ numberOfLines={1}
24
+ style={{
25
+ fontSize: theme.t(`TextSizeHeading${size}Mobile`).number,
26
+ color: theme.t(`ColorText${color}${invertedSuffix}`),
27
+ }}
28
+ >
29
+ {title}
30
+ </Header>
31
+ );
32
+ }
@@ -0,0 +1,143 @@
1
+ import React from 'react';
2
+ import { Image, TouchableOpacity } from 'react-native';
3
+ import { fireEvent, render } from '@testing-library/react-native';
4
+ import { ICardProps, PxCard, PxCardMediaProps } from '..';
5
+ import { type RenderResult } from '@testing-library/react-native/build/render';
6
+ import { LaunchArguments } from 'react-native-launch-arguments';
7
+ import LinearGradient from 'react-native-linear-gradient';
8
+
9
+ describe('PxCard', () => {
10
+ let component: RenderResult;
11
+ const onPress = jest.fn();
12
+
13
+ beforeEach(() => {
14
+ (LaunchArguments.value as jest.Mock).mockReturnValue({ e2e: false });
15
+ });
16
+
17
+ afterEach(() => {
18
+ jest.resetAllMocks();
19
+ });
20
+
21
+ it('renders title and (optional) content', () => {
22
+ givenComponent();
23
+ thenItMustDisplayTheTitleAndContent();
24
+ });
25
+
26
+ it('fires onPress when the card is pressed', () => {
27
+ givenComponent();
28
+ whenClickingCard();
29
+ thenOnPressIsCalled();
30
+ });
31
+
32
+ it('computes aspectRatio for mediaPosition="top" from Image.getSize', () => {
33
+ givenImageSize(1200, 600); // aspectRatio = 2
34
+ givenComponent();
35
+ thenMediaHasAspectRatio(2);
36
+ });
37
+
38
+ it('sets width-based layout for mediaPosition="left"', () => {
39
+ givenImageSize(600, 1200);
40
+ givenComponent({
41
+ mediaPosition: 'left',
42
+ });
43
+ thenMediaHasWidth(120);
44
+ });
45
+
46
+ it('shows gradient colors when contrastHelperGradient is true', () => {
47
+ givenComponent({
48
+ contrastHelper: 'gradient',
49
+ gradientDirection: 'horizontal',
50
+ });
51
+ thenHelperGradientIsDisplayed();
52
+ });
53
+
54
+ it('shows overlay colors when contrastHelperOverlay is true', () => {
55
+ givenComponent({
56
+ contrastHelper: 'overlay',
57
+ });
58
+ thenHelperOverlayIsDisplayed();
59
+ });
60
+ //
61
+ it('does not render MediaPart when mediaPosition="background"', () => {
62
+ givenComponent({
63
+ mediaPosition: 'background',
64
+ });
65
+ thenMediaIsDisplayedBackground();
66
+ });
67
+
68
+ function givenComponent(props?: Partial<ICardProps & PxCardMediaProps>) {
69
+ component = render(
70
+ <PxCard
71
+ {...props}
72
+ content={'Short description'}
73
+ alternativeText={'Nice Image'}
74
+ src={{
75
+ uri: 'https://example.com/img.jpg',
76
+ }}
77
+ onPress={onPress}
78
+ title={'Promo title'}
79
+ />
80
+ );
81
+ }
82
+
83
+ function givenImageSize(width: number, height: number) {
84
+ jest.spyOn(Image, 'getSize').mockImplementation((_, success) => {
85
+ success(width, height);
86
+ });
87
+ }
88
+
89
+ function thenItMustDisplayTheTitleAndContent() {
90
+ const { getByText } = component!;
91
+ const title = getByText('Promo title');
92
+ expect(title).toBeOnTheScreen();
93
+ expect(getByText('Short description')).toBeOnTheScreen();
94
+ }
95
+
96
+ function whenClickingCard() {
97
+ const { UNSAFE_getByType } = component;
98
+ fireEvent.press(UNSAFE_getByType(TouchableOpacity));
99
+ }
100
+
101
+ function thenOnPressIsCalled() {
102
+ expect(onPress).toHaveBeenCalled();
103
+ }
104
+
105
+ function thenMediaHasAspectRatio(aspectRatio: number) {
106
+ const { UNSAFE_queryAllByType } = component!;
107
+ const media = UNSAFE_queryAllByType(Image)?.[0];
108
+ expect(media?.props?.style.aspectRatio).toEqual(aspectRatio);
109
+ }
110
+
111
+ async function thenMediaHasWidth(width: number) {
112
+ const { UNSAFE_getAllByType } = component!;
113
+ const media = UNSAFE_getAllByType(Image)?.[0];
114
+ expect(media?.props?.style.width).toEqual(width);
115
+ }
116
+
117
+ function thenMediaIsDisplayedBackground() {
118
+ const { UNSAFE_getAllByType } = component!;
119
+ const media = UNSAFE_getAllByType(Image);
120
+ expect(media.length).toEqual(1);
121
+ }
122
+
123
+ function thenHelperOverlayIsDisplayed() {
124
+ const { UNSAFE_getAllByType } = component!;
125
+ const overlay = UNSAFE_getAllByType(LinearGradient)?.[0];
126
+ expect(overlay?.props?.colors).toEqual([
127
+ 'rgba(255,255,255,0.7)',
128
+ 'rgba(255,255,255,0.7)',
129
+ ]);
130
+ }
131
+
132
+ function thenHelperGradientIsDisplayed() {
133
+ const { UNSAFE_getAllByType } = component!;
134
+ const gradient = UNSAFE_getAllByType(LinearGradient)?.[0];
135
+ expect(gradient.props.colors).toEqual([
136
+ '#FFFFFF',
137
+ '#FFFFFFDD',
138
+ '#FFFFFF00',
139
+ ]);
140
+ expect(gradient.props.start).toEqual({ x: 0, y: 1.0 });
141
+ expect(gradient.props.end).toEqual({ x: 1.0, y: 1.0 });
142
+ }
143
+ });
@@ -0,0 +1,80 @@
1
+ import type { RenderResult } from '@testing-library/react-native/build/render';
2
+ import { LaunchArguments } from 'react-native-launch-arguments';
3
+ import { render } from '@testing-library/react-native';
4
+ import React from 'react';
5
+ import { PxMessageCard, PxMessageCardProps } from '../PxMessageCard';
6
+
7
+ describe('PxCard', () => {
8
+ let component: RenderResult;
9
+ const onPress = jest.fn();
10
+
11
+ beforeEach(() => {
12
+ (LaunchArguments.value as jest.Mock).mockReturnValue({ e2e: false });
13
+ });
14
+
15
+ afterEach(() => {
16
+ jest.resetAllMocks();
17
+ });
18
+
19
+ it('renders title and (optional) content', () => {
20
+ (LaunchArguments.value as jest.Mock).mockReturnValue({ e2e: true });
21
+ givenComponent();
22
+ thenItMustDisplayTitleContentCTACloseButton();
23
+ });
24
+
25
+ it('should be accessible', () => {
26
+ givenComponent({
27
+ accessibilityLabel: 'accessibilityLabel',
28
+ accessibilityHint: 'accessibilityHint',
29
+ closeButtonAccessibilityLabel: 'closeButtonAccessibilityLabel',
30
+ closeButtonAccessibilityHint: 'closeButtonAccessibilityHint',
31
+ });
32
+ thenItMustBeAccessible();
33
+ });
34
+
35
+ it('should display label properly', () => {
36
+ givenComponent({
37
+ tag: 'Tag label',
38
+ });
39
+ thenItMustDisplayTagLabel();
40
+ });
41
+
42
+ function givenComponent(props?: Partial<PxMessageCardProps>) {
43
+ component = render(
44
+ <PxMessageCard
45
+ {...props}
46
+ testID={'testID'}
47
+ content={'Short description'}
48
+ alternativeText={'Nice Image'}
49
+ src={{
50
+ uri: 'https://example.com/img.jpg',
51
+ }}
52
+ onPress={onPress}
53
+ title={'Promo title'}
54
+ ctaLabel={'Call to action'}
55
+ onClose={() => {}}
56
+ />
57
+ );
58
+ }
59
+
60
+ function thenItMustDisplayTitleContentCTACloseButton() {
61
+ const { getByText, getByTestId } = component!;
62
+ expect(getByText('Promo title')).toBeOnTheScreen();
63
+ expect(getByText('Short description')).toBeOnTheScreen();
64
+ expect(getByText('Call to action')).toBeOnTheScreen();
65
+ expect(getByTestId('testID-close-button')).toBeOnTheScreen();
66
+ }
67
+
68
+ function thenItMustBeAccessible() {
69
+ const { getByLabelText, getByA11yHint } = component!;
70
+ expect(getByLabelText('accessibilityLabel')).toBeOnTheScreen();
71
+ expect(getByA11yHint('accessibilityHint')).toBeOnTheScreen();
72
+ expect(getByLabelText('closeButtonAccessibilityLabel')).toBeOnTheScreen();
73
+ expect(getByA11yHint('closeButtonAccessibilityHint')).toBeOnTheScreen();
74
+ }
75
+
76
+ function thenItMustDisplayTagLabel() {
77
+ const { getByText } = component!;
78
+ expect(getByText('Tag label')).toBeOnTheScreen();
79
+ }
80
+ });
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './PxCard';
2
+ export * from './PxMessageCard';
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../../tsconfig.npm.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ },
6
+ "include": ["src","../../../lib.d.ts"],
7
+ "exclude": ["dist", "node_modules", "src/**/*.test.ts", "src/**/*.spec.ts"]
8
+ }