@telus-uds/components-base 3.31.0 → 3.32.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.
@@ -2,10 +2,12 @@ import React from 'react';
2
2
  import View from "react-native-web/dist/exports/View";
3
3
  import PropTypes from 'prop-types';
4
4
  import { useThemeTokensCallback } from '../ThemeProvider';
5
- import { a11yProps, getTokensPropType, selectSystemProps, variantProp, viewProps } from '../utils';
5
+ import { a11yProps, getTokensPropType, selectSystemProps, useCopy, variantProp, viewProps } from '../utils';
6
6
  import { StackWrap } from '../StackView';
7
7
  import Typography from '../Typography';
8
8
  import ColourBubble from './ColourBubble';
9
+ import { UNAVAILABLE_VARIANT, DICTIONARY_CONTENT_SHAPE } from './constants';
10
+ import DEFAULT_COLOUR_TOGGLE_DICTIONARY from './dictionary';
9
11
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
12
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps]);
11
13
  const ColourToggle = /*#__PURE__*/React.forwardRef((_ref, ref) => {
@@ -16,37 +18,49 @@ const ColourToggle = /*#__PURE__*/React.forwardRef((_ref, ref) => {
16
18
  items,
17
19
  onChange,
18
20
  showTooltips,
21
+ copy = 'en',
22
+ dictionary = DEFAULT_COLOUR_TOGGLE_DICTIONARY,
19
23
  ...rest
20
24
  } = _ref;
21
25
  const [currentColourId, setCurrentColourId] = React.useState(defaultColourId);
22
26
  const getTokens = useThemeTokensCallback('ColourToggle', tokens, variant);
27
+ const getCopy = useCopy({
28
+ dictionary,
29
+ copy
30
+ });
23
31
  const {
24
32
  space
25
33
  } = getTokens();
26
- const {
27
- colourName: currentColourName = ''
28
- } = items.find(item => item.id === currentColourId) || '';
34
+ const currentItem = items.find(_ref2 => {
35
+ let {
36
+ id
37
+ } = _ref2;
38
+ return id === currentColourId;
39
+ });
29
40
  return /*#__PURE__*/_jsxs(View, {
30
41
  ref: ref,
31
42
  ...selectProps(rest),
32
43
  children: [/*#__PURE__*/_jsx(Typography, {
33
- children: currentColourName
44
+ children: currentItem?.unavailable ? `${currentItem.colourName} ${getCopy('unavailable')}` : currentItem?.colourName ?? ''
34
45
  }), /*#__PURE__*/_jsx(StackWrap, {
35
46
  space: space,
36
47
  accessibilityRole: "radiogroup",
37
- children: items.map((_ref2, index) => {
48
+ children: items.map((_ref3, index) => {
38
49
  let {
39
50
  id,
40
51
  colourHexCode,
41
- colourName
42
- } = _ref2;
52
+ colourName,
53
+ unavailable,
54
+ disabled
55
+ } = _ref3;
43
56
  const colourBubbleId = id || `ColourBubble[${index}]`;
44
57
  const handleChangeColour = event => {
45
58
  setCurrentColourId(id);
46
59
  onChange?.(event, {
47
60
  id,
48
61
  colourHexCode,
49
- colourName
62
+ colourName,
63
+ unavailable
50
64
  });
51
65
  };
52
66
  return /*#__PURE__*/_jsx(ColourBubble, {
@@ -56,7 +70,9 @@ const ColourToggle = /*#__PURE__*/React.forwardRef((_ref, ref) => {
56
70
  colourHexCode: colourHexCode,
57
71
  colourName: colourName,
58
72
  onPress: handleChangeColour,
59
- showTooltip: showTooltips
73
+ showTooltip: showTooltips,
74
+ unavailable: unavailable,
75
+ isDisabled: !!disabled
60
76
  }, colourBubbleId);
61
77
  })
62
78
  })]
@@ -83,7 +99,9 @@ ColourToggle.propTypes = {
83
99
  items: PropTypes.arrayOf(PropTypes.exact({
84
100
  colourHexCode: PropTypes.string,
85
101
  colourName: PropTypes.string,
86
- id: PropTypes.string
102
+ id: PropTypes.string,
103
+ unavailable: PropTypes.oneOf(Object.values(UNAVAILABLE_VARIANT)),
104
+ disabled: PropTypes.bool
87
105
  })),
88
106
  /**
89
107
  * If provided, this function is called when the current selection of the color is changed of all currently `items`. Receives two parameters: item object selected and the event
@@ -92,6 +110,17 @@ ColourToggle.propTypes = {
92
110
  /**
93
111
  * When true, displays each colour's name as a tooltip on hover (web only).
94
112
  */
95
- showTooltips: PropTypes.bool
113
+ showTooltips: PropTypes.bool,
114
+ /**
115
+ * Select English or French copy.
116
+ */
117
+ copy: PropTypes.oneOf(['en', 'fr']),
118
+ /**
119
+ * Override default labels.
120
+ */
121
+ dictionary: PropTypes.shape({
122
+ en: DICTIONARY_CONTENT_SHAPE,
123
+ fr: DICTIONARY_CONTENT_SHAPE
124
+ })
96
125
  };
97
126
  export default ColourToggle;
@@ -0,0 +1,8 @@
1
+ import PropTypes from 'prop-types';
2
+ export const UNAVAILABLE_VARIANT = {
3
+ DARK: 'dark',
4
+ LIGHT: 'light'
5
+ };
6
+ export const DICTIONARY_CONTENT_SHAPE = PropTypes.shape({
7
+ unavailable: PropTypes.string
8
+ });
@@ -0,0 +1,9 @@
1
+ const DEFAULT_COLOUR_TOGGLE_DICTIONARY = {
2
+ en: {
3
+ unavailable: '(Out of stock)'
4
+ },
5
+ fr: {
6
+ unavailable: '(En rupture de stock)'
7
+ }
8
+ };
9
+ export default DEFAULT_COLOUR_TOGGLE_DICTIONARY;
@@ -199,7 +199,11 @@ const Validator = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
199
199
 
200
200
  // Sync external value prop to internal state
201
201
  React.useEffect(() => {
202
- if (value && Number(value).toString() !== 'NaN') {
202
+ if (value === '') {
203
+ setCodes(Array(validatorsLength).fill(''));
204
+ return;
205
+ }
206
+ if (Number(value).toString() !== 'NaN') {
203
207
  const digits = value.split('').slice(0, validatorsLength);
204
208
  const newCodes = Array(validatorsLength).fill('');
205
209
  digits.forEach((digit, i) => {
package/lib/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  "@gorhom/portal": "^1.0.14",
13
13
  "@react-native-picker/picker": "^2.9.0",
14
14
  "@telus-uds/system-constants": "^3.0.0",
15
- "@telus-uds/system-theme-tokens": "^4.21.0",
15
+ "@telus-uds/system-theme-tokens": "^4.22.0",
16
16
  "airbnb-prop-types": "^2.16.0",
17
17
  "css-mediaquery": "^0.1.2",
18
18
  "expo-document-picker": "^13.0.1",
@@ -84,6 +84,6 @@
84
84
  "standard-engine": {
85
85
  "skip": true
86
86
  },
87
- "version": "3.31.0",
87
+ "version": "3.32.0",
88
88
  "types": "types/index.d.ts"
89
89
  }
package/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  "@gorhom/portal": "^1.0.14",
13
13
  "@react-native-picker/picker": "^2.9.0",
14
14
  "@telus-uds/system-constants": "^3.0.0",
15
- "@telus-uds/system-theme-tokens": "^4.21.0",
15
+ "@telus-uds/system-theme-tokens": "^4.22.0",
16
16
  "airbnb-prop-types": "^2.16.0",
17
17
  "css-mediaquery": "^0.1.2",
18
18
  "expo-document-picker": "^13.0.1",
@@ -84,6 +84,6 @@
84
84
  "standard-engine": {
85
85
  "skip": true
86
86
  },
87
- "version": "3.31.0",
87
+ "version": "3.32.0",
88
88
  "types": "types/index.d.ts"
89
89
  }
@@ -28,6 +28,8 @@ const [selectItemProps, selectedItemPropTypes] = selectSystemProps([
28
28
  pressProps,
29
29
  viewProps
30
30
  ])
31
+ const EQUAL_WIDTH = 'equal'
32
+ const RESPONSIVE_WIDTH = 'responsive'
31
33
 
32
34
  const ButtonGroup = React.forwardRef(
33
35
  (
@@ -70,6 +72,9 @@ const ButtonGroup = React.forwardRef(
70
72
  const themeButtonTokensCallback = useThemeTokensCallback('ButtonGroupItem', tokens, variant)
71
73
  const gapValue = useSpacingScale(gap || space)
72
74
 
75
+ const isWeb = Platform.OS === 'web'
76
+ const buttonWidthVariant = variant?.width
77
+
73
78
  const getButtonTokens = useCallback(
74
79
  (state) => {
75
80
  const themeButtonTokens = themeButtonTokensCallback(state)
@@ -77,36 +82,93 @@ const ButtonGroup = React.forwardRef(
77
82
  const shouldUseTransparentBackground =
78
83
  isMobileNonContained && !state.selected && !state.pressed && !state.hover && !state.focus
79
84
 
85
+ let widthStyle
86
+
87
+ switch (buttonWidthVariant) {
88
+ case EQUAL_WIDTH:
89
+ widthStyle = staticStyles.equalWidth
90
+ break
91
+ case RESPONSIVE_WIDTH:
92
+ widthStyle = staticStyles.responsiveWidth
93
+ break
94
+ // no default
95
+ }
96
+
80
97
  return {
81
98
  ...themeButtonTokens,
82
- ...(variant?.width === 'equal' && staticStyles.equalWidth),
99
+ ...widthStyle,
83
100
  ...(shouldUseTransparentBackground && { backgroundColor: 'transparent' }),
84
101
  alignSelf: themeButtonTokens.width ? 'flex-start' : 'center'
85
102
  }
86
103
  },
87
- [themeButtonTokensCallback, isMobileNonContained, variant?.width]
104
+ [themeButtonTokensCallback, isMobileNonContained, buttonWidthVariant]
88
105
  )
89
106
 
90
- const fieldsetStyles = useMemo(
91
- () => ({
92
- ...staticStyles.fieldsetBase,
107
+ const fieldsetStyles = useMemo(() => {
108
+ let fieldSetBase
109
+ switch (buttonWidthVariant) {
110
+ case EQUAL_WIDTH:
111
+ case RESPONSIVE_WIDTH:
112
+ fieldSetBase = staticStyles.fieldSetResponsive
113
+ break
114
+ default:
115
+ fieldSetBase = staticStyles.fieldsetBase
116
+ break
117
+ }
118
+
119
+ return {
120
+ ...fieldSetBase,
93
121
  borderRadius,
94
122
  backgroundColor: isMobileNonContained ? 'transparent' : backgroundColor || 'transparent',
95
- padding,
96
- width: variant?.width === 'equal' ? '100%' : 'auto'
97
- }),
98
- [borderRadius, backgroundColor, padding, variant?.width, isMobileNonContained]
99
- )
123
+ padding
124
+ }
125
+ }, [borderRadius, backgroundColor, padding, isMobileNonContained, buttonWidthVariant])
100
126
 
101
- const viewStyles = useMemo(
102
- () => ({
103
- ...staticStyles.viewBase,
127
+ const buttonsContainerStyles = useMemo(() => {
128
+ let flexWrap
129
+ if (isWeb) {
130
+ flexWrap = 'wrap'
131
+ } else {
132
+ switch (buttonWidthVariant) {
133
+ case EQUAL_WIDTH:
134
+ case RESPONSIVE_WIDTH:
135
+ flexWrap = 'nowrap'
136
+ break
137
+ default:
138
+ flexWrap = 'wrap'
139
+ break
140
+ }
141
+ }
142
+
143
+ let justifyContent
144
+ switch (buttonWidthVariant) {
145
+ case EQUAL_WIDTH:
146
+ justifyContent = 'space-evenly'
147
+ break
148
+ default:
149
+ justifyContent = 'flex-start'
150
+ break
151
+ }
152
+
153
+ let viewBaseStyle
154
+ switch (buttonWidthVariant) {
155
+ case EQUAL_WIDTH:
156
+ case RESPONSIVE_WIDTH:
157
+ viewBaseStyle = {}
158
+ break
159
+ default:
160
+ viewBaseStyle = staticStyles.viewBase
161
+ break
162
+ }
163
+
164
+ return {
165
+ ...viewBaseStyle,
166
+ flexWrap,
104
167
  flexDirection: direction === 'column' ? 'column' : 'row',
105
168
  gap: gapValue || 0,
106
- justifyContent: variant?.width === 'equal' ? 'space-evenly' : 'flex-start'
107
- }),
108
- [direction, gapValue, variant?.width]
109
- )
169
+ justifyContent
170
+ }
171
+ }, [direction, gapValue, buttonWidthVariant, isWeb])
110
172
 
111
173
  const { currentValues, toggleOneValue } = useMultipleInputValues({
112
174
  initialValues,
@@ -128,7 +190,7 @@ const ButtonGroup = React.forwardRef(
128
190
  }
129
191
 
130
192
  // Some web screenreaders e.g. MacOS Voiceover don't handle radiogroups properly unless radio is direct child of radiogroup
131
- const innerRole =
193
+ const buttonsWrapperRole =
132
194
  Platform.OS === 'web' && accessibilityRole === 'radiogroup' ? accessibilityRole : undefined
133
195
 
134
196
  return (
@@ -149,7 +211,7 @@ const ButtonGroup = React.forwardRef(
149
211
  style={fieldsetStyles}
150
212
  {...selectProps(rest)}
151
213
  >
152
- <View accessibilityRole={innerRole} style={viewStyles}>
214
+ <View accessibilityRole={buttonsWrapperRole} style={buttonsContainerStyles}>
153
215
  {items.map(
154
216
  (
155
217
  {
@@ -329,17 +391,24 @@ ButtonGroup.propTypes = {
329
391
  }
330
392
 
331
393
  const staticStyles = StyleSheet.create({
394
+ equalWidth: {
395
+ width: '100%',
396
+ flex: 1
397
+ },
398
+ responsiveWidth: {
399
+ width: 'auto'
400
+ },
332
401
  fieldsetBase: {
333
402
  alignSelf: 'flex-start',
334
- display: 'inline'
403
+ display: 'inline',
404
+ width: 'auto'
405
+ },
406
+ fieldSetResponsive: {
407
+ width: '100%'
335
408
  },
336
409
  viewBase: {
337
410
  flexWrap: 'wrap',
338
411
  alignItems: 'center'
339
- },
340
- equalWidth: {
341
- width: '100%',
342
- flex: 1
343
412
  }
344
413
  })
345
414
 
@@ -14,9 +14,7 @@ const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, vie
14
14
 
15
15
  const GRID_COLUMNS = 12
16
16
 
17
- const isOverlayColor = (color) => {
18
- return color && typeof color === 'string' && color.startsWith('rgba(')
19
- }
17
+ const isOverlayColor = (color) => color && typeof color === 'string' && color.startsWith('rgba(')
20
18
 
21
19
  const setBackgroundImage = ({
22
20
  src,
@@ -25,7 +23,8 @@ const setBackgroundImage = ({
25
23
  backgroundImagePosition,
26
24
  backgroundImageAlign,
27
25
  content,
28
- cardStyle
26
+ cardStyle,
27
+ testID
29
28
  }) => {
30
29
  const borderRadius = cardStyle?.borderRadius || 0
31
30
  const borderWidth = cardStyle?.borderWidth || 0
@@ -91,6 +90,7 @@ const setBackgroundImage = ({
91
90
  style={[staticStyles.imageBackground, backgroundImageStyle]}
92
91
  role="img"
93
92
  aria-label={alt}
93
+ testID={testID}
94
94
  >
95
95
  {content}
96
96
  </View>
@@ -108,6 +108,7 @@ const setBackgroundImage = ({
108
108
  accessible={true}
109
109
  accessibilityLabel={alt}
110
110
  accessibilityIgnoresInvertColors={true}
111
+ testID={testID}
111
112
  />
112
113
  <View style={staticStyles.contentOverlay}>{content}</View>
113
114
  </View>
@@ -122,33 +123,18 @@ const setBackgroundImage = ({
122
123
  style={staticStyles.imageBackground}
123
124
  accessible={true}
124
125
  accessibilityLabel={alt}
126
+ testID={testID}
125
127
  >
126
128
  {content}
127
129
  </ImageBackground>
128
130
  )
129
131
  }
130
132
 
131
- const selectPaddedContentStyles = ({
133
+ const selectPaddedContentStyles = ({ paddingTop, paddingBottom, paddingLeft, paddingRight }) => ({
132
134
  paddingTop,
133
135
  paddingBottom,
134
136
  paddingLeft,
135
- paddingRight,
136
- borderWidth,
137
- borderColor,
138
- borderRadius,
139
- hasInteractiveBorder
140
- }) => ({
141
- paddingTop,
142
- paddingBottom,
143
- paddingLeft,
144
- paddingRight,
145
- ...(hasInteractiveBorder
146
- ? {
147
- borderWidth,
148
- borderColor,
149
- borderRadius
150
- }
151
- : {})
137
+ paddingRight
152
138
  })
153
139
 
154
140
  const selectInteractiveOverlayStyles = ({ backgroundColor, borderRadius, borderWidth }) => {
@@ -162,32 +148,49 @@ const selectInteractiveOverlayStyles = ({ backgroundColor, borderRadius, borderW
162
148
  backgroundColor,
163
149
  borderRadius: adjustedBorderRadius,
164
150
  pointerEvents: 'none',
165
- zIndex: 1
151
+ zIndex: 3
166
152
  }
167
153
  }
168
154
 
169
- // Ensure explicit selection of tokens
170
- export const selectStyles = ({
171
- flex,
155
+ const selectOuterContainerStyles = ({
156
+ containerStyle,
172
157
  backgroundColor,
173
- borderColor,
174
158
  borderRadius,
159
+ borderColor,
175
160
  borderWidth,
176
- paddingBottom,
177
- paddingLeft,
178
- paddingRight,
179
- paddingTop,
180
- marginTop,
181
- marginBottom,
182
- marginLeft,
183
- marginRight,
184
- minWidth,
185
- shadow,
186
- backgroundGradient,
187
- gradient,
188
- maxHeight,
189
- overflowY
190
- }) => {
161
+ hasGradientToken
162
+ }) => ({
163
+ ...containerStyle,
164
+ backgroundColor,
165
+ borderRadius,
166
+ ...(hasGradientToken ? {} : { borderColor, borderWidth })
167
+ })
168
+
169
+ // Ensure explicit selection of tokens
170
+ export const selectStyles = (
171
+ {
172
+ flex,
173
+ backgroundColor,
174
+ borderColor,
175
+ borderRadius,
176
+ borderWidth,
177
+ paddingBottom,
178
+ paddingLeft,
179
+ paddingRight,
180
+ paddingTop,
181
+ marginTop,
182
+ marginBottom,
183
+ marginLeft,
184
+ marginRight,
185
+ minWidth,
186
+ shadow,
187
+ backgroundGradient,
188
+ gradient,
189
+ maxHeight,
190
+ overflowY
191
+ },
192
+ { hasBgImage = false } = {}
193
+ ) => {
191
194
  const hasGradient = (gradient || backgroundGradient) && Platform.OS === 'web'
192
195
 
193
196
  let backgroundImageValue = null
@@ -226,7 +229,9 @@ export const selectStyles = ({
226
229
  ? {
227
230
  backgroundImage: backgroundImageValue,
228
231
  backgroundOrigin: `border-box`,
229
- boxShadow: `inset 0 1000px ${boxShadowColor}`,
232
+ // bgImage child fills the content-box and replaces what the inset shadow
233
+ // used to cover, so we omit it to avoid hiding the image.
234
+ ...(hasBgImage ? {} : { boxShadow: `inset 0 1000px ${boxShadowColor}` }),
230
235
  border: `${borderWidth}px solid transparent`
231
236
  }
232
237
  : {}),
@@ -244,14 +249,13 @@ export const selectStyles = ({
244
249
  * intended to be used in apps or sites directly: build themed components on top of this.
245
250
  */
246
251
  const CardBase = React.forwardRef(
247
- ({ children, tokens, dataSet, backgroundImage, fullBleedContent, cardState, ...rest }, ref) => {
252
+ (
253
+ { children, tokens, dataSet, backgroundImage, fullBleedContent, cardState, testID, ...rest },
254
+ ref
255
+ ) => {
248
256
  const resolvedTokens = typeof tokens === 'function' ? tokens(cardState) : tokens
249
- const tokensToUse =
250
- backgroundImage && backgroundImage.src
251
- ? { ...resolvedTokens, gradient: undefined, backgroundGradient: undefined }
252
- : resolvedTokens
253
-
254
- const cardStyle = selectStyles(tokensToUse)
257
+ const hasBgImage = Boolean(backgroundImage && backgroundImage.src)
258
+ const cardStyle = selectStyles(resolvedTokens, { hasBgImage })
255
259
  const props = selectProps(rest)
256
260
 
257
261
  let content = children
@@ -283,31 +287,28 @@ const CardBase = React.forwardRef(
283
287
  } = cardStyle
284
288
 
285
289
  const hasPadding = paddingTop || paddingBottom || paddingLeft || paddingRight
286
- const hasInteractiveBorder = borderWidth && borderWidth > 0
287
290
  const hasInteractiveOverlay = isOverlayColor(backgroundColor)
291
+ const outerBackgroundColor = hasInteractiveOverlay ? undefined : backgroundColor
292
+ const hasGradientToken = Boolean(resolvedTokens.gradient) && Platform.OS === 'web'
288
293
 
289
- const paddedContent =
290
- hasPadding || hasInteractiveBorder ? (
291
- <View
292
- style={selectPaddedContentStyles({
293
- paddingTop,
294
- paddingBottom,
295
- paddingLeft,
296
- paddingRight,
297
- borderWidth,
298
- borderColor,
299
- borderRadius,
300
- hasInteractiveBorder
301
- })}
302
- >
303
- {children}
304
- </View>
305
- ) : (
306
- children
307
- )
294
+ const paddedContent = hasPadding ? (
295
+ <View
296
+ style={selectPaddedContentStyles({
297
+ paddingTop,
298
+ paddingBottom,
299
+ paddingLeft,
300
+ paddingRight
301
+ })}
302
+ >
303
+ {children}
304
+ </View>
305
+ ) : (
306
+ children
307
+ )
308
308
 
309
309
  const contentWithOverlay = (
310
310
  <>
311
+ <View style={staticStyles.contentOverlay}>{paddedContent}</View>
311
312
  {hasInteractiveOverlay && Platform.OS === 'web' && (
312
313
  <View
313
314
  style={selectInteractiveOverlayStyles({
@@ -315,9 +316,9 @@ const CardBase = React.forwardRef(
315
316
  borderRadius,
316
317
  borderWidth
317
318
  })}
319
+ testID={testID && `${testID}-card-base-bg-overlay`}
318
320
  />
319
321
  )}
320
- <View style={staticStyles.contentOverlay}>{paddedContent}</View>
321
322
  </>
322
323
  )
323
324
 
@@ -328,11 +329,25 @@ const CardBase = React.forwardRef(
328
329
  backgroundImagePosition,
329
330
  backgroundImageAlign,
330
331
  content: contentWithOverlay,
331
- cardStyle: { ...containerStyle, borderRadius }
332
+ cardStyle: { borderRadius, borderWidth },
333
+ testID: testID && `${testID}-card-base-bg-image`
332
334
  })
333
335
 
334
336
  return (
335
- <View style={{ ...containerStyle, borderRadius }} dataSet={dataSet} ref={ref} {...props}>
337
+ <View
338
+ style={selectOuterContainerStyles({
339
+ containerStyle,
340
+ backgroundColor: outerBackgroundColor,
341
+ borderRadius,
342
+ borderColor,
343
+ borderWidth,
344
+ hasGradientToken
345
+ })}
346
+ dataSet={dataSet}
347
+ ref={ref}
348
+ testID={testID}
349
+ {...props}
350
+ >
336
351
  {content}
337
352
  </View>
338
353
  )
@@ -365,7 +380,7 @@ const CardBase = React.forwardRef(
365
380
  )
366
381
 
367
382
  return (
368
- <View style={containerStyle} dataSet={dataSet} ref={ref} {...props}>
383
+ <View style={containerStyle} dataSet={dataSet} ref={ref} testID={testID} {...props}>
369
384
  <FlexGrid>
370
385
  <FlexGridRow>
371
386
  {imageFirst ? (
@@ -386,7 +401,7 @@ const CardBase = React.forwardRef(
386
401
  }
387
402
 
388
403
  return (
389
- <View style={cardStyle} dataSet={dataSet} ref={ref} {...props}>
404
+ <View style={cardStyle} dataSet={dataSet} ref={ref} testID={testID} {...props}>
390
405
  {content}
391
406
  </View>
392
407
  )
@@ -434,6 +449,10 @@ CardBase.propTypes = {
434
449
  ...selectedSystemPropTypes,
435
450
  children: PropTypes.node,
436
451
  tokens: getTokensPropType('Card'),
452
+ /**
453
+ * Identifier for testing purposes.
454
+ */
455
+ testID: PropTypes.string,
437
456
  /**
438
457
  * Apply background image to the card.
439
458
  */