@telus-uds/components-base 1.75.0 → 1.77.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +31 -2
  2. package/lib/Box/Box.js +112 -7
  3. package/lib/Box/backgroundImageStylesMap.js +101 -0
  4. package/lib/Carousel/CarouselThumbnail.js +10 -4
  5. package/lib/Carousel/CarouselThumbnailNavigation.js +3 -3
  6. package/lib/ExpandCollapse/Panel.js +21 -10
  7. package/lib/Footnote/Footnote.js +9 -13
  8. package/lib/Link/ChevronLink.js +2 -0
  9. package/lib/Link/InlinePressable.js +15 -2
  10. package/lib/Link/LinkBase.js +1 -0
  11. package/lib/OrderedList/OrderedList.js +21 -20
  12. package/lib/PriceLockup/PriceLockup.js +220 -0
  13. package/lib/PriceLockup/index.js +10 -0
  14. package/lib/PriceLockup/utils/renderFootnoteContent.js +93 -0
  15. package/lib/PriceLockup/utils/renderFootnoteLinks.js +36 -0
  16. package/lib/PriceLockup/utils/renderPrice.js +147 -0
  17. package/lib/PriceLockup/utils/renderTypography.js +31 -0
  18. package/lib/Skeleton/Skeleton.js +6 -3
  19. package/lib/index.js +8 -0
  20. package/lib-module/Box/Box.js +115 -9
  21. package/lib-module/Box/backgroundImageStylesMap.js +94 -0
  22. package/lib-module/Carousel/CarouselThumbnail.js +10 -4
  23. package/lib-module/Carousel/CarouselThumbnailNavigation.js +3 -3
  24. package/lib-module/ExpandCollapse/Panel.js +21 -10
  25. package/lib-module/Footnote/Footnote.js +9 -13
  26. package/lib-module/Link/ChevronLink.js +2 -0
  27. package/lib-module/Link/InlinePressable.js +16 -2
  28. package/lib-module/Link/LinkBase.js +1 -0
  29. package/lib-module/OrderedList/OrderedList.js +21 -20
  30. package/lib-module/PriceLockup/PriceLockup.js +214 -0
  31. package/lib-module/PriceLockup/index.js +2 -0
  32. package/lib-module/PriceLockup/utils/renderFootnoteContent.js +87 -0
  33. package/lib-module/PriceLockup/utils/renderFootnoteLinks.js +28 -0
  34. package/lib-module/PriceLockup/utils/renderPrice.js +141 -0
  35. package/lib-module/PriceLockup/utils/renderTypography.js +23 -0
  36. package/lib-module/Skeleton/Skeleton.js +6 -3
  37. package/lib-module/index.js +1 -0
  38. package/package.json +2 -2
  39. package/src/Box/Box.jsx +120 -9
  40. package/src/Box/backgroundImageStylesMap.js +21 -0
  41. package/src/Carousel/CarouselThumbnail.jsx +8 -6
  42. package/src/Carousel/CarouselThumbnailNavigation.jsx +3 -4
  43. package/src/ExpandCollapse/Panel.jsx +16 -10
  44. package/src/Footnote/Footnote.jsx +3 -6
  45. package/src/Link/ChevronLink.jsx +5 -1
  46. package/src/Link/InlinePressable.jsx +36 -15
  47. package/src/Link/LinkBase.jsx +1 -0
  48. package/src/OrderedList/OrderedList.jsx +17 -20
  49. package/src/PriceLockup/PriceLockup.jsx +218 -0
  50. package/src/PriceLockup/index.js +3 -0
  51. package/src/PriceLockup/utils/renderFootnoteContent.jsx +77 -0
  52. package/src/PriceLockup/utils/renderFootnoteLinks.jsx +38 -0
  53. package/src/PriceLockup/utils/renderPrice.jsx +201 -0
  54. package/src/PriceLockup/utils/renderTypography.jsx +13 -0
  55. package/src/Skeleton/Skeleton.jsx +8 -3
  56. package/src/index.js +1 -0
  57. package/types/Typography.d.ts +1 -0
package/src/Box/Box.jsx CHANGED
@@ -1,18 +1,21 @@
1
- import React, { forwardRef } from 'react'
1
+ import React, { forwardRef, useEffect, useState } from 'react'
2
2
  import PropTypes from 'prop-types'
3
- import { View, ScrollView, Platform } from 'react-native'
3
+ import { View, ScrollView, Platform, StyleSheet, ImageBackground, Image } from 'react-native'
4
4
  import { useThemeTokens } from '../ThemeProvider'
5
5
  import {
6
6
  a11yProps,
7
7
  getA11yPropsFromHtmlTag,
8
8
  getTokensPropType,
9
9
  layoutTags,
10
+ responsiveProps,
10
11
  selectSystemProps,
11
12
  spacingProps,
13
+ useResponsiveProp,
12
14
  useSpacingScale,
13
15
  variantProp,
14
16
  viewProps
15
17
  } from '../utils'
18
+ import backgroundImageStylesMap from './backgroundImageStylesMap'
16
19
 
17
20
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
18
21
 
@@ -69,6 +72,50 @@ const selectBoxStyles = (
69
72
  return styles
70
73
  }
71
74
 
75
+ const setBackgroundImage = ({
76
+ src,
77
+ alt,
78
+ backgroundImageResizeMode,
79
+ backgroundImagePosition,
80
+ backgroundImageAlign,
81
+ backgroundImageWidth,
82
+ backgroundImageHeight,
83
+ content
84
+ }) => {
85
+ if (backgroundImageResizeMode === 'contain') {
86
+ const containedViewStyle = {
87
+ ...staticStyles.containedView,
88
+ width: backgroundImageWidth,
89
+ height: backgroundImageHeight,
90
+ ...backgroundImageStylesMap[`${backgroundImagePosition}-${backgroundImageAlign}`]
91
+ }
92
+
93
+ return (
94
+ <View style={staticStyles.containedContainer}>
95
+ <View style={containedViewStyle}>
96
+ <Image
97
+ source={src}
98
+ alt={alt}
99
+ style={staticStyles.containedImage}
100
+ accessibilityIgnoresInvertColors
101
+ />
102
+ </View>
103
+ {content}
104
+ </View>
105
+ )
106
+ }
107
+ return (
108
+ <ImageBackground
109
+ source={src}
110
+ alt={alt}
111
+ style={staticStyles.backgroundImageContainer}
112
+ resizeMode={backgroundImageResizeMode}
113
+ >
114
+ {content}
115
+ </ImageBackground>
116
+ )
117
+ }
118
+
72
119
  /**
73
120
  * A layout utility component. Use Box to create space (padding) around content.
74
121
  *
@@ -154,6 +201,7 @@ const Box = forwardRef(
154
201
  testID,
155
202
  dataSet,
156
203
  customGradient,
204
+ backgroundImage,
157
205
  ...rest
158
206
  },
159
207
  ref
@@ -174,23 +222,52 @@ const Box = forwardRef(
174
222
  ...selectBoxStyles(themeTokens, customGradient)
175
223
  }
176
224
 
177
- const childrenToRender =
178
- typeof customGradient === 'function'
179
- ? customGradient(styles.colors, styles)(children)
180
- : children
225
+ let content = children
226
+ if (typeof customGradient === 'function')
227
+ content = customGradient(styles.colors, styles)(children)
228
+
229
+ const { src = '', alt = '', resizeMode = '', position = '', align = '' } = backgroundImage || {}
230
+ const backgroundImageResizeMode = useResponsiveProp(resizeMode, 'cover')
231
+ const backgroundImagePosition = useResponsiveProp(position, 'none')
232
+ const backgroundImageAlign = useResponsiveProp(align, 'stretch')
233
+ const [backgroundImageWidth, setBackgroundImageWidth] = useState(0)
234
+ const [backgroundImageHeight, setBackgroundImageHeight] = useState(0)
235
+ if (backgroundImage)
236
+ content = setBackgroundImage({
237
+ src,
238
+ alt,
239
+ backgroundImageResizeMode,
240
+ backgroundImagePosition,
241
+ backgroundImageAlign,
242
+ backgroundImageWidth,
243
+ backgroundImageHeight,
244
+ content
245
+ })
246
+
247
+ useEffect(() => {
248
+ if (backgroundImage && backgroundImageWidth === 0 && backgroundImageHeight === 0) {
249
+ Image.getSize(src, (width, height) => {
250
+ // Only update the state if the size has changed
251
+ if (width !== backgroundImageWidth || height !== backgroundImageHeight) {
252
+ setBackgroundImageWidth(width)
253
+ setBackgroundImageHeight(height)
254
+ }
255
+ })
256
+ }
257
+ }, [backgroundImage, backgroundImageWidth, backgroundImageHeight, src])
181
258
 
182
259
  if (scroll) {
183
260
  const scrollProps = typeof scroll === 'object' ? scroll : {}
184
261
  scrollProps.contentContainerStyle = [styles, scrollProps.contentContainerStyle]
185
262
  return (
186
263
  <ScrollView {...scrollProps} {...props} testID={testID} dataSet={dataSet} ref={ref}>
187
- {childrenToRender}
264
+ {content}
188
265
  </ScrollView>
189
266
  )
190
267
  }
191
268
  return (
192
269
  <View {...props} style={styles} testID={testID} dataSet={dataSet} ref={ref}>
193
- {childrenToRender}
270
+ {content}
194
271
  </View>
195
272
  )
196
273
  }
@@ -284,7 +361,41 @@ Box.propTypes = {
284
361
  /**
285
362
  Use this prop if need to add a custom gradient for mobile
286
363
  */
287
- customGradient: PropTypes.func
364
+ customGradient: PropTypes.func,
365
+ /**
366
+ * Use this prop to add a background image to the box.
367
+ */
368
+ backgroundImage: PropTypes.shape({
369
+ src: PropTypes.string.isRequired,
370
+ alt: PropTypes.string,
371
+ resizeMode: responsiveProps.getTypeOptionallyByViewport(
372
+ PropTypes.oneOf(['cover', 'contain', 'stretch', 'repeat', 'center'])
373
+ ),
374
+ position: responsiveProps.getTypeOptionallyByViewport(
375
+ PropTypes.oneOf(['none', 'bottom', 'left', 'right', 'top'])
376
+ ),
377
+ align: responsiveProps.getTypeOptionallyByViewport(
378
+ PropTypes.oneOf(['start', 'end', 'center', 'stretch'])
379
+ )
380
+ })
288
381
  }
289
382
 
290
383
  export default Box
384
+
385
+ const staticStyles = StyleSheet.create({
386
+ backgroundImageContainer: {
387
+ flex: 1
388
+ },
389
+ containedContainer: {
390
+ flex: 1,
391
+ overflow: 'hidden'
392
+ },
393
+ containedView: {
394
+ zIndex: -1,
395
+ position: 'absolute'
396
+ },
397
+ containedImage: {
398
+ width: '100%',
399
+ height: '100%'
400
+ }
401
+ })
@@ -0,0 +1,21 @@
1
+ export default {
2
+ 'top-start': { top: 0 },
3
+ 'top-center': { left: 0, right: 0, marginHorizontal: 'auto' },
4
+ 'top-end': { top: 0, right: 0 },
5
+ 'right-start': { top: 0, right: 0 },
6
+ 'left-start': { top: 0 },
7
+ 'left-center': { top: 0, bottom: 0, marginVertical: 'auto' },
8
+ 'none-start': { top: 0, bottom: 0, marginVertical: 'auto' },
9
+ 'none-center': { top: 0, bottom: 0, left: 0, right: 0, margin: 'auto' },
10
+ 'right-center': { top: 0, bottom: 0, right: 0, marginVertical: 'auto' },
11
+ 'none-end': { top: 0, bottom: 0, right: 0, marginVertical: 'auto' },
12
+ 'bottom-start': { bottom: 0, left: 0 },
13
+ 'left-end': { bottom: 0, left: 0 },
14
+ 'bottom-center': { left: 0, right: 0, bottom: 0, marginHorizontal: 'auto' },
15
+ 'bottom-end': { right: 0, bottom: 0 },
16
+ 'right-end': { right: 0, bottom: 0 },
17
+ 'top-stretch': { left: 0, right: 0, width: '100%' },
18
+ 'left-stretch': { top: 0, bottom: 0, height: '100%' },
19
+ 'right-stretch': { top: 0, bottom: 0, right: 0, height: '100%' },
20
+ 'bottom-stretch': { left: 0, right: 0, bottom: 0, width: '100%' }
21
+ }
@@ -32,7 +32,7 @@ const CarouselThumbnail = ({ accessibilityLabel, alt, index, src }) => {
32
32
  // Allow using the spacebar for navigation
33
33
  if (event?.key === ' ') goTo(index)
34
34
  }
35
- const { borderWidth, padding, selectedBorderColor, selectedBorderWidth, size } =
35
+ const { borderWidth, padding, selectedBorderColor, selectedBorderWidth, size, margin } =
36
36
  getThumbnailTokens({ viewport })
37
37
  const styles = {
38
38
  image: {
@@ -42,7 +42,12 @@ const CarouselThumbnail = ({ accessibilityLabel, alt, index, src }) => {
42
42
  selected: {
43
43
  borderColor: selectedBorderColor,
44
44
  borderWidth: selectedBorderWidth,
45
- padding: padding - selectedBorderWidth + borderWidth
45
+ padding: padding - selectedBorderWidth,
46
+ marginBottom: margin + selectedBorderWidth
47
+ },
48
+ nonSelected: {
49
+ padding: padding - borderWidth,
50
+ marginBottom: margin + selectedBorderWidth
46
51
  }
47
52
  }
48
53
 
@@ -62,10 +67,7 @@ const CarouselThumbnail = ({ accessibilityLabel, alt, index, src }) => {
62
67
 
63
68
  return [
64
69
  pressableStyles,
65
- index === activeIndex && styles.selected,
66
- {
67
- outline: 'none'
68
- }
70
+ index === activeIndex ? [styles.selected, { outline: 'none' }] : styles.nonSelected
69
71
  ]
70
72
  }}
71
73
  >
@@ -14,20 +14,19 @@ const CarouselThumbnailNavigation = forwardRef(({ thumbnails = [] }, ref) => {
14
14
  if (thumbnails.length !== totalItems) {
15
15
  throw new Error('Thumbnail set provided does not match the number of slides in the carousel')
16
16
  }
17
- const { containerPaddingTop: thumbnailContainerPaddingTop, margin: thumbnailMargin } =
18
- useThemeTokens('CarouselThumbnail')
17
+ const { containerPaddingTop: thumbnailContainerPaddingTop } = useThemeTokens('CarouselThumbnail')
19
18
  const stackWrapTokens = {
20
19
  justifyContent: 'flex-start'
21
20
  }
22
21
  const containerStyles = {
23
22
  justifyContent: 'center',
24
23
  alignItems,
25
- paddingTop: thumbnailContainerPaddingTop - thumbnailMargin
24
+ paddingTop: thumbnailContainerPaddingTop
26
25
  }
27
26
 
28
27
  return (
29
28
  <View style={containerStyles}>
30
- <StackWrap direction="row" tokens={stackWrapTokens} ref={ref}>
29
+ <StackWrap direction="row" tokens={stackWrapTokens} space={2} ref={ref}>
31
30
  {thumbnails.map(({ accessibilityLabel, alt, src }, index) => (
32
31
  <CarouselThumbnail
33
32
  accessibilityLabel={accessibilityLabel}
@@ -64,6 +64,10 @@ const selectContentPanelStyles = ({
64
64
  marginBottom
65
65
  })
66
66
 
67
+ const selectControlPanelStyles = ({ contentPanelBackgroundColor }) => ({
68
+ backgroundColor: contentPanelBackgroundColor
69
+ })
70
+
67
71
  /**
68
72
  * An item in an `ExpandCollapse` which contains collapsible `children` and a `control` that opens
69
73
  * and closes the collapsible children when pressed.
@@ -136,16 +140,18 @@ const ExpandCollapsePanel = forwardRef(
136
140
  </View>
137
141
  ) : (
138
142
  <View ref={ref} style={themeTokens}>
139
- <ExpandCollapseControl
140
- {...selectedProps}
141
- isExpanded={isExpanded}
142
- tokens={controlTokens}
143
- variant={variant}
144
- onPress={handleControlPress}
145
- ref={controlRef}
146
- >
147
- {control}
148
- </ExpandCollapseControl>
143
+ <View style={selectControlPanelStyles(themeTokens)}>
144
+ <ExpandCollapseControl
145
+ {...selectedProps}
146
+ isExpanded={isExpanded}
147
+ tokens={controlTokens}
148
+ variant={variant}
149
+ onPress={handleControlPress}
150
+ ref={controlRef}
151
+ >
152
+ {control}
153
+ </ExpandCollapseControl>
154
+ </View>
149
155
  {isExpanded && (
150
156
  <View
151
157
  style={{
@@ -156,13 +156,10 @@ const Footnote = ({
156
156
  }
157
157
 
158
158
  return (
159
- // TODO: Extract the OrderedList.Item from the array when the issue #4361 is fixed
160
159
  <OrderedList start={number}>
161
- {[
162
- <OrderedList.Item key={number}>
163
- <Text style={selectCustomContentFontStyle(themeTokens)}>{content}</Text>
164
- </OrderedList.Item>
165
- ]}
160
+ <OrderedList.Item key={number}>
161
+ <Text style={selectCustomContentFontStyle(themeTokens)}>{content}</Text>
162
+ </OrderedList.Item>
166
163
  </OrderedList>
167
164
  )
168
165
  }, [content, number, themeTokens])
@@ -12,7 +12,10 @@ import LinkBase from './LinkBase'
12
12
  * ChevronLink is not intended to be deeply themable; variants passed to ChevronLink are forwarded to Link.
13
13
  */
14
14
  const ChevronLink = forwardRef(
15
- ({ direction = 'right', children, tokens = {}, variant, dataSet, ...otherlinkProps }, ref) => {
15
+ (
16
+ { direction = 'right', children, tokens = {}, variant, dataSet, onPress, ...otherlinkProps },
17
+ ref
18
+ ) => {
16
19
  const getChevronTokens = useThemeTokensCallback('ChevronLink', tokens, variant)
17
20
  const applyChevronTokens = (linkState) => {
18
21
  const { leftIcon, rightIcon, iconDisplace, height, fontSize, ...otherTokens } =
@@ -38,6 +41,7 @@ const ChevronLink = forwardRef(
38
41
  tokens={getTokens}
39
42
  dataSet={dataSet}
40
43
  ref={ref}
44
+ onPress={onPress}
41
45
  >
42
46
  {children}
43
47
  </LinkBase>
@@ -1,5 +1,5 @@
1
- import React, { forwardRef } from 'react'
2
- import { Pressable, StyleSheet } from 'react-native'
1
+ import React, { forwardRef, useCallback } from 'react'
2
+ import { Pressable, StyleSheet, Platform } from 'react-native'
3
3
 
4
4
  /**
5
5
  * @typedef {import('react-native').PressableProps} PressableProps
@@ -14,19 +14,40 @@ import { Pressable, StyleSheet } from 'react-native'
14
14
  * @param {PressableProps} PressableProps
15
15
  */
16
16
  // React Native exports prop Types but not propTypes, use JSDoc types here rather than duplicate RN
17
- // eslint-disable-next-line react/prop-types
18
- const InlinePressable = forwardRef(({ children, inlineFlex = true, style, ...props }, ref) => (
19
- <Pressable
20
- ref={ref}
21
- style={(pressState) => [
22
- typeof style === 'function' ? style(pressState) : style,
23
- staticStyles[inlineFlex ? 'inlineFlex' : 'inline']
24
- ]}
25
- {...props}
26
- >
27
- {(pressState) => (typeof children === 'function' ? children(pressState) : children)}
28
- </Pressable>
29
- ))
17
+ /* eslint-disable react/prop-types */
18
+ const InlinePressable = forwardRef(
19
+ ({ children, inlineFlex = true, style, onPress, ...props }, ref) => {
20
+ const handlePress = useCallback(() => {
21
+ if (onPress) {
22
+ onPress()
23
+ }
24
+ }, [onPress])
25
+
26
+ const handleKeyPress = useCallback(
27
+ (e) => {
28
+ if (e.key === 'Enter' || e.key === ' ') {
29
+ handlePress()
30
+ }
31
+ },
32
+ [handlePress]
33
+ )
34
+
35
+ return (
36
+ <Pressable
37
+ ref={ref}
38
+ style={(pressState) => [
39
+ typeof style === 'function' ? style(pressState) : style,
40
+ staticStyles[inlineFlex ? 'inlineFlex' : 'inline']
41
+ ]}
42
+ onPress={handlePress}
43
+ onKeyDown={Platform.OS === 'web' ? handleKeyPress : undefined}
44
+ {...props}
45
+ >
46
+ {(pressState) => (typeof children === 'function' ? children(pressState) : children)}
47
+ </Pressable>
48
+ )
49
+ }
50
+ )
30
51
  InlinePressable.displayName = 'InlinePressable'
31
52
 
32
53
  const staticStyles = StyleSheet.create({
@@ -162,6 +162,7 @@ const LinkBase = forwardRef(
162
162
  inlineFlex={hasIcon}
163
163
  // assuming links without icons should be inline (even if they are long)
164
164
  ref={ref}
165
+ keyboardShouldPersistTaps="handled"
165
166
  style={(linkState) => {
166
167
  const themeTokens = resolveLinkTokens(linkState)
167
168
  const outerBorderStyles = selectOuterBorderStyles(themeTokens)
@@ -7,29 +7,26 @@ import Item from './Item'
7
7
 
8
8
  const [selectProps, selectedSystemPropsTypes] = selectSystemProps([htmlAttrs, viewProps])
9
9
 
10
- const getChildrenWithParentVariants = (variant, children, start) => {
11
- if (variant)
12
- return children.map((child, i) => {
10
+ const OrderedList = forwardRef(({ children, start, variant, ...rest }, ref) => {
11
+ const childrenWithParentVariants = useMemo(() => {
12
+ const addVariantToProps = (child, i, isLastChild) => {
13
13
  const existingChildVariants = child.props?.variant ?? {}
14
- return {
15
- ...child,
16
- props: {
17
- ...child.props,
18
- index: start + i,
19
- isLastChild: i === children.length - 1,
20
- variant: { ...existingChildVariants, ...variant }
21
- }
14
+ return React.cloneElement(child, {
15
+ index: start + i,
16
+ isLastChild,
17
+ variant: { ...existingChildVariants, ...variant }
18
+ })
19
+ }
20
+
21
+ if (variant) {
22
+ if (Array.isArray(children)) {
23
+ return children.map((child, i) => addVariantToProps(child, i, i === children.length - 1))
22
24
  }
23
- })
24
-
25
- return children
26
- }
25
+ return [addVariantToProps(children, 0, true)]
26
+ }
27
27
 
28
- const OrderedList = forwardRef(({ children, start, variant, ...rest }, ref) => {
29
- const childrenWithParentVariants = useMemo(
30
- () => getChildrenWithParentVariants(variant, children, start),
31
- [children, variant, start]
32
- )
28
+ return children
29
+ }, [children, variant, start])
33
30
 
34
31
  return (
35
32
  <OrderedListBase ref={ref} {...selectProps(rest)}>
@@ -0,0 +1,218 @@
1
+ import React from 'react'
2
+ import { StyleSheet, View } from 'react-native'
3
+ import PropTypes from 'prop-types'
4
+ import Divider from '../Divider'
5
+ import { useViewport } from '../ViewportProvider'
6
+ import { useThemeTokens } from '../ThemeProvider'
7
+ import { selectSystemProps, getTokensPropType, htmlAttrs, viewProps } from '../utils'
8
+ import renderFootnoteContent from './utils/renderFootnoteContent'
9
+ import renderPrice from './utils/renderPrice'
10
+ import renderTypography from './utils/renderTypography'
11
+
12
+ const [selectProps, selectedSystemPropTypes] = selectSystemProps([htmlAttrs, viewProps])
13
+
14
+ const selectTopTextTypographyTokens = ({ topTextFontSize, topTextLineHeight }) => ({
15
+ fontSize: topTextFontSize,
16
+ lineHeight: topTextLineHeight
17
+ })
18
+
19
+ const selectCurrencySymbolTypographyTokens = ({
20
+ currencySymbolFontSize,
21
+ currencySymbolLineHeight,
22
+ currencySymbolFontWeight
23
+ }) => ({
24
+ fontSize: currencySymbolFontSize,
25
+ lineHeight: currencySymbolLineHeight,
26
+ fontWeight: currencySymbolFontWeight
27
+ })
28
+
29
+ const selectAmountTypographyTokens = ({
30
+ amountFontSize,
31
+ amountLineHeight,
32
+ amountLetterSpacing,
33
+ amountFontWeight,
34
+ fontColor
35
+ }) => {
36
+ // This is used to apply the proper line height to the amount
37
+ const lineHeightMultiplier = 1.18
38
+ return {
39
+ color: fontColor,
40
+ fontSize: amountFontSize,
41
+ lineHeight: amountLineHeight * lineHeightMultiplier,
42
+ letterSpacing: amountLetterSpacing,
43
+ fontWeight: amountFontWeight
44
+ }
45
+ }
46
+
47
+ const selectCentsTypographyTokens = ({ centsFontSize, centsLineHeight, centsFontWeight }) => ({
48
+ fontSize: centsFontSize,
49
+ lineHeight: centsLineHeight,
50
+ fontWeight: centsFontWeight
51
+ })
52
+
53
+ const selectRateTypographyTokens = ({ rateFontSize, rateLineHeight, rateFontWeight }) => ({
54
+ fontSize: rateFontSize,
55
+ lineHeight: rateLineHeight,
56
+ fontWeight: rateFontWeight
57
+ })
58
+
59
+ const selectBottomTextTypographyTokens = ({ bottomTextFontSize, bottomTextLineHeight }) => ({
60
+ fontSize: bottomTextFontSize,
61
+ lineHeight: bottomTextLineHeight
62
+ })
63
+
64
+ const PriceLockup = ({
65
+ size = 'medium',
66
+ signDirection = 'left',
67
+ footnoteLinks,
68
+ topText,
69
+ price,
70
+ currencySymbol = '$',
71
+ rateText,
72
+ ratePosition = 'right',
73
+ bottomText,
74
+ onClickFootnote,
75
+ strikeThrough,
76
+ a11yText,
77
+ tokens: priceLockupTokens,
78
+ variant = {},
79
+ ...rest
80
+ }) => {
81
+ const viewport = useViewport()
82
+ const {
83
+ footnoteMarginTop,
84
+ footnoteGap,
85
+ bottomTextMarginTop,
86
+ priceMarginBottom,
87
+ bottomLinksMarginLeft,
88
+ topTextMarginBottom,
89
+ fontColor,
90
+ dividerColor,
91
+ ...themeTokens
92
+ } = useThemeTokens(
93
+ 'PriceLockup',
94
+ priceLockupTokens,
95
+ { ...variant, size },
96
+ { viewport, strikeThrough }
97
+ )
98
+ const currencySymbolTypographyTokens = selectCurrencySymbolTypographyTokens(themeTokens)
99
+ const amountTypographyTokens = selectAmountTypographyTokens(themeTokens)
100
+ const centsTypographyTokens = selectCentsTypographyTokens(themeTokens)
101
+ const rateTypographyTokens = selectRateTypographyTokens(themeTokens)
102
+ const topTextTypographyTokens = selectTopTextTypographyTokens(themeTokens)
103
+ const bottomTextTypographyTokens = selectBottomTextTypographyTokens(themeTokens)
104
+
105
+ return (
106
+ <View style={[staticStyles.priceLockupContainer, { ...selectProps(rest) }]}>
107
+ {topText ? <View>{renderTypography(topText, topTextTypographyTokens)}</View> : null}
108
+ {renderPrice(
109
+ price,
110
+ rateText,
111
+ ratePosition,
112
+ signDirection,
113
+ currencySymbol,
114
+ currencySymbolTypographyTokens,
115
+ amountTypographyTokens,
116
+ centsTypographyTokens,
117
+ rateTypographyTokens,
118
+ fontColor,
119
+ strikeThrough,
120
+ a11yText,
121
+ bottomText,
122
+ bottomLinksMarginLeft,
123
+ footnoteLinks,
124
+ onClickFootnote,
125
+ themeTokens
126
+ )}
127
+ {bottomText ? (
128
+ <>
129
+ <Divider
130
+ testID="price-lockup-divider"
131
+ role="separator"
132
+ tokens={{ color: dividerColor }}
133
+ />
134
+ {renderFootnoteContent(
135
+ footnoteMarginTop,
136
+ bottomTextMarginTop,
137
+ bottomText,
138
+ bottomTextTypographyTokens,
139
+ fontColor,
140
+ footnoteLinks,
141
+ bottomLinksMarginLeft,
142
+ onClickFootnote,
143
+ themeTokens
144
+ )}
145
+ </>
146
+ ) : null}
147
+ </View>
148
+ )
149
+ }
150
+
151
+ PriceLockup.displayName = 'PriceLockup'
152
+ PriceLockup.propTypes = {
153
+ ...selectedSystemPropTypes,
154
+ /**
155
+ * Size of the component
156
+ *
157
+ * Small for pricing in product catalogue pages, medium for pricing in product comparison cards.
158
+ */
159
+ size: PropTypes.oneOf(['small', 'medium']),
160
+ /**
161
+ * If currency symbol other than `$` to be used
162
+ */
163
+ currencySymbol: PropTypes.string,
164
+ /**
165
+ * Shows additional info above the price
166
+ */
167
+ topText: PropTypes.string,
168
+ /**
169
+ * Monetary value (including decimals separated by ".")
170
+ */
171
+ price: PropTypes.string.isRequired,
172
+ /**
173
+ * Shows month/year unit
174
+ */
175
+ rateText: PropTypes.string,
176
+ /**
177
+ * Shows additional info below the price with a `Divider`
178
+ */
179
+ bottomText: PropTypes.string,
180
+ /**
181
+ * Displays which side the currency should appear (left, right)
182
+ */
183
+ signDirection: PropTypes.oneOf(['right', 'left']),
184
+ /**
185
+ * Displays where the rate should appear (bottom, right)
186
+ */
187
+ ratePosition: PropTypes.oneOf(['right', 'bottom']),
188
+ /**
189
+ * Shows additional link for context
190
+ */
191
+ footnoteLinks: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])),
192
+ /**
193
+ * Function to be called when a footnote link is clicked
194
+ */
195
+ onClickFootnote: PropTypes.func,
196
+ /**
197
+ * To show price savings comparison
198
+ */
199
+ strikeThrough: PropTypes.bool,
200
+ /**
201
+ * To provide a11y text for `PriceLockup` component
202
+ *
203
+ * **Note:** a11yText will override strikethrough price, so it must include price (ie. "was 50 dollars per month")
204
+ */
205
+ a11yText: PropTypes.string,
206
+ /**
207
+ * `PriceLockup` tokens
208
+ */
209
+ tokens: getTokensPropType('PriceLockup')
210
+ }
211
+
212
+ export default PriceLockup
213
+
214
+ const staticStyles = StyleSheet.create({
215
+ priceLockupContainer: {
216
+ alignSelf: 'flex-start'
217
+ }
218
+ })
@@ -0,0 +1,3 @@
1
+ import PriceLockup from './PriceLockup'
2
+
3
+ export default PriceLockup