@telus-uds/components-web 1.8.0 → 1.10.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 (188) hide show
  1. package/CHANGELOG.md +41 -2
  2. package/lib/Autocomplete/Autocomplete.js +393 -0
  3. package/lib/Autocomplete/Loading.js +51 -0
  4. package/lib/Autocomplete/Suggestions.js +81 -0
  5. package/lib/Autocomplete/constants.js +19 -0
  6. package/lib/Autocomplete/dictionary.js +19 -0
  7. package/lib/Autocomplete/index.js +13 -0
  8. package/lib/Breadcrumbs/Breadcrumbs.js +8 -3
  9. package/lib/Callout/Callout.js +3 -0
  10. package/lib/Card/Card.js +180 -0
  11. package/lib/Card/CardContent.js +110 -0
  12. package/lib/Card/CardFooter.js +98 -0
  13. package/lib/Card/index.js +13 -0
  14. package/lib/Countdown/Countdown.js +189 -0
  15. package/lib/Countdown/Segment.js +111 -0
  16. package/lib/Countdown/constants.js +14 -0
  17. package/lib/Countdown/dictionary.js +29 -0
  18. package/lib/Countdown/index.js +13 -0
  19. package/lib/Countdown/types.js +39 -0
  20. package/lib/Countdown/useCountdown.js +40 -0
  21. package/lib/Footnote/Footnote.js +67 -24
  22. package/lib/Modal/ModalContent.js +11 -4
  23. package/lib/OptimizeImage/OptimizeImage.js +127 -0
  24. package/lib/OptimizeImage/index.js +13 -0
  25. package/lib/OptimizeImage/utils/getFallbackUrl.js +18 -0
  26. package/lib/OptimizeImage/utils/getOptimizedUrl.js +32 -0
  27. package/lib/OptimizeImage/utils/hasWebpSupport.js +38 -0
  28. package/lib/OptimizeImage/utils/index.js +31 -0
  29. package/lib/OptimizeImage/utils/isSvgUrl.js +10 -0
  30. package/lib/QuantitySelector/QuantitySelector.js +253 -0
  31. package/lib/QuantitySelector/dictionary.js +33 -0
  32. package/lib/QuantitySelector/index.js +13 -0
  33. package/lib/QuantitySelector/styles.js +40 -0
  34. package/lib/StoryCard/StoryCard.js +244 -0
  35. package/lib/StoryCard/index.js +13 -0
  36. package/lib/TermsAndConditions/ExpandCollapse.js +141 -0
  37. package/lib/TermsAndConditions/TermsAndConditions.js +221 -0
  38. package/lib/TermsAndConditions/dictionary.js +19 -0
  39. package/lib/TermsAndConditions/index.js +15 -0
  40. package/lib/Testimonial/Testimonial.js +226 -0
  41. package/lib/Testimonial/index.js +13 -0
  42. package/lib/Toast/Toast.js +15 -8
  43. package/lib/Video/ControlBar/ControlBar.js +315 -0
  44. package/lib/Video/ControlBar/Controls/VideoButton/VideoButton.js +91 -0
  45. package/lib/Video/ControlBar/Controls/VideoMenu/VideoMenu.js +186 -0
  46. package/lib/Video/ControlBar/Controls/VideoProgressBar/VideoProgressBar.js +221 -0
  47. package/lib/Video/ControlBar/Controls/VolumeSlider/VolumeSlider.js +213 -0
  48. package/lib/Video/MiddleControlButton/MiddleControlButton.js +89 -0
  49. package/lib/Video/Video.js +1072 -0
  50. package/lib/Video/index.js +13 -0
  51. package/lib/Video/videoText.js +62 -0
  52. package/lib/WebVideo/WebVideo.js +170 -0
  53. package/lib/WebVideo/index.js +13 -0
  54. package/lib/baseExports.js +0 -6
  55. package/lib/index.js +91 -1
  56. package/lib/shared/VideoSplash/SplashButton/SplashButton.js +102 -0
  57. package/lib/shared/VideoSplash/SplashButtonWithDetails/SplashButtonWithDetails.js +234 -0
  58. package/lib/shared/VideoSplash/VideoSplash.js +86 -0
  59. package/lib/shared/VideoSplash/helpers.js +38 -0
  60. package/lib/utils/index.js +8 -0
  61. package/lib-module/Autocomplete/Autocomplete.js +369 -0
  62. package/lib-module/Autocomplete/Loading.js +38 -0
  63. package/lib-module/Autocomplete/Suggestions.js +64 -0
  64. package/lib-module/Autocomplete/constants.js +5 -0
  65. package/lib-module/Autocomplete/dictionary.js +12 -0
  66. package/lib-module/Autocomplete/index.js +2 -0
  67. package/lib-module/Breadcrumbs/Breadcrumbs.js +8 -3
  68. package/lib-module/Callout/Callout.js +3 -0
  69. package/lib-module/Card/Card.js +158 -0
  70. package/lib-module/Card/CardContent.js +92 -0
  71. package/lib-module/Card/CardFooter.js +80 -0
  72. package/lib-module/Card/index.js +2 -0
  73. package/lib-module/Countdown/Countdown.js +165 -0
  74. package/lib-module/Countdown/Segment.js +94 -0
  75. package/lib-module/Countdown/constants.js +4 -0
  76. package/lib-module/Countdown/dictionary.js +22 -0
  77. package/lib-module/Countdown/index.js +2 -0
  78. package/lib-module/Countdown/types.js +23 -0
  79. package/lib-module/Countdown/useCountdown.js +32 -0
  80. package/lib-module/Footnote/Footnote.js +65 -24
  81. package/lib-module/Modal/ModalContent.js +10 -4
  82. package/lib-module/OptimizeImage/OptimizeImage.js +106 -0
  83. package/lib-module/OptimizeImage/index.js +2 -0
  84. package/lib-module/OptimizeImage/utils/getFallbackUrl.js +8 -0
  85. package/lib-module/OptimizeImage/utils/getOptimizedUrl.js +22 -0
  86. package/lib-module/OptimizeImage/utils/hasWebpSupport.js +32 -0
  87. package/lib-module/OptimizeImage/utils/index.js +4 -0
  88. package/lib-module/OptimizeImage/utils/isSvgUrl.js +3 -0
  89. package/lib-module/QuantitySelector/QuantitySelector.js +232 -0
  90. package/lib-module/QuantitySelector/dictionary.js +26 -0
  91. package/lib-module/QuantitySelector/index.js +2 -0
  92. package/lib-module/QuantitySelector/styles.js +21 -0
  93. package/lib-module/StoryCard/StoryCard.js +220 -0
  94. package/lib-module/StoryCard/index.js +2 -0
  95. package/lib-module/TermsAndConditions/ExpandCollapse.js +120 -0
  96. package/lib-module/TermsAndConditions/TermsAndConditions.js +193 -0
  97. package/lib-module/TermsAndConditions/dictionary.js +12 -0
  98. package/lib-module/TermsAndConditions/index.js +1 -0
  99. package/lib-module/Testimonial/Testimonial.js +204 -0
  100. package/lib-module/Testimonial/index.js +2 -0
  101. package/lib-module/Toast/Toast.js +15 -8
  102. package/lib-module/Video/ControlBar/ControlBar.js +292 -0
  103. package/lib-module/Video/ControlBar/Controls/VideoButton/VideoButton.js +74 -0
  104. package/lib-module/Video/ControlBar/Controls/VideoMenu/VideoMenu.js +167 -0
  105. package/lib-module/Video/ControlBar/Controls/VideoProgressBar/VideoProgressBar.js +201 -0
  106. package/lib-module/Video/ControlBar/Controls/VolumeSlider/VolumeSlider.js +193 -0
  107. package/lib-module/Video/MiddleControlButton/MiddleControlButton.js +72 -0
  108. package/lib-module/Video/Video.js +1042 -0
  109. package/lib-module/Video/index.js +2 -0
  110. package/lib-module/Video/videoText.js +55 -0
  111. package/lib-module/WebVideo/WebVideo.js +144 -0
  112. package/lib-module/WebVideo/index.js +2 -0
  113. package/lib-module/baseExports.js +1 -1
  114. package/lib-module/index.js +10 -0
  115. package/lib-module/shared/VideoSplash/SplashButton/SplashButton.js +85 -0
  116. package/lib-module/shared/VideoSplash/SplashButtonWithDetails/SplashButtonWithDetails.js +216 -0
  117. package/lib-module/shared/VideoSplash/VideoSplash.js +65 -0
  118. package/lib-module/shared/VideoSplash/helpers.js +23 -0
  119. package/lib-module/utils/index.js +2 -1
  120. package/package.json +7 -5
  121. package/src/Autocomplete/Autocomplete.jsx +354 -0
  122. package/src/Autocomplete/Loading.jsx +18 -0
  123. package/src/Autocomplete/Suggestions.jsx +52 -0
  124. package/src/Autocomplete/constants.js +6 -0
  125. package/src/Autocomplete/dictionary.js +12 -0
  126. package/src/Autocomplete/index.js +3 -0
  127. package/src/Breadcrumbs/Breadcrumbs.jsx +4 -3
  128. package/src/Callout/Callout.jsx +1 -1
  129. package/src/Card/Card.jsx +170 -0
  130. package/src/Card/CardContent.jsx +88 -0
  131. package/src/Card/CardFooter.jsx +70 -0
  132. package/src/Card/index.js +3 -0
  133. package/src/Countdown/Countdown.jsx +144 -0
  134. package/src/Countdown/Segment.jsx +69 -0
  135. package/src/Countdown/constants.js +4 -0
  136. package/src/Countdown/dictionary.js +22 -0
  137. package/src/Countdown/index.js +3 -0
  138. package/src/Countdown/types.js +23 -0
  139. package/src/Countdown/useCountdown.js +34 -0
  140. package/src/Footnote/Footnote.jsx +73 -23
  141. package/src/Modal/ModalContent.jsx +8 -4
  142. package/src/OptimizeImage/OptimizeImage.jsx +131 -0
  143. package/src/OptimizeImage/index.js +3 -0
  144. package/src/OptimizeImage/utils/getFallbackUrl.js +9 -0
  145. package/src/OptimizeImage/utils/getOptimizedUrl.js +30 -0
  146. package/src/OptimizeImage/utils/hasWebpSupport.js +33 -0
  147. package/src/OptimizeImage/utils/index.js +5 -0
  148. package/src/OptimizeImage/utils/isSvgUrl.js +3 -0
  149. package/src/QuantitySelector/QuantitySelector.jsx +245 -0
  150. package/src/QuantitySelector/dictionary.js +27 -0
  151. package/src/QuantitySelector/index.js +3 -0
  152. package/src/QuantitySelector/styles.js +83 -0
  153. package/src/StoryCard/StoryCard.jsx +198 -0
  154. package/src/StoryCard/index.js +3 -0
  155. package/src/TermsAndConditions/ExpandCollapse.jsx +106 -0
  156. package/src/TermsAndConditions/TermsAndConditions.jsx +161 -0
  157. package/src/TermsAndConditions/dictionary.js +12 -0
  158. package/src/TermsAndConditions/index.js +1 -0
  159. package/src/Testimonial/Testimonial.jsx +169 -0
  160. package/src/Testimonial/index.js +3 -0
  161. package/src/Toast/Toast.jsx +12 -5
  162. package/src/Video/ControlBar/ControlBar.jsx +261 -0
  163. package/src/Video/ControlBar/Controls/VideoButton/VideoButton.jsx +61 -0
  164. package/src/Video/ControlBar/Controls/VideoMenu/VideoMenu.jsx +159 -0
  165. package/src/Video/ControlBar/Controls/VideoProgressBar/VideoProgressBar.jsx +185 -0
  166. package/src/Video/ControlBar/Controls/VolumeSlider/VolumeSlider.jsx +184 -0
  167. package/src/Video/MiddleControlButton/MiddleControlButton.jsx +64 -0
  168. package/src/Video/Video.jsx +988 -0
  169. package/src/Video/index.js +3 -0
  170. package/src/Video/videoText.js +58 -0
  171. package/src/WebVideo/WebVideo.jsx +131 -0
  172. package/src/WebVideo/index.js +3 -0
  173. package/src/baseExports.js +0 -1
  174. package/src/index.js +10 -0
  175. package/src/shared/VideoSplash/SplashButton/SplashButton.jsx +64 -0
  176. package/src/shared/VideoSplash/SplashButtonWithDetails/SplashButtonWithDetails.jsx +128 -0
  177. package/src/shared/VideoSplash/VideoSplash.jsx +50 -0
  178. package/src/shared/VideoSplash/helpers.js +27 -0
  179. package/src/utils/index.js +10 -1
  180. package/types/Autocomplete.d.ts +32 -0
  181. package/types/Card.d.ts +45 -0
  182. package/types/ControlBar.d.ts +59 -0
  183. package/types/MiddleControlButton.d.ts +15 -0
  184. package/types/Video.d.ts +39 -0
  185. package/types/VideoButton.d.ts +14 -0
  186. package/types/VideoMenu.d.ts +16 -0
  187. package/types/VideoProgressBar.d.ts +17 -0
  188. package/types/VolumeSlider.d.ts +20 -0
@@ -0,0 +1,88 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import {
4
+ getTokensPropType,
5
+ selectSystemProps,
6
+ useThemeTokens,
7
+ useViewport,
8
+ variantProp
9
+ } from '@telus-uds/components-base'
10
+ import styled from 'styled-components'
11
+ import { htmlAttrs } from '../utils'
12
+
13
+ const [selectProps, selectedSystemPropTypes] = selectSystemProps([htmlAttrs])
14
+
15
+ const CardContentContainer = styled.div(
16
+ ({
17
+ backgroundColor,
18
+ borderRadius,
19
+ paddingBottom,
20
+ paddingLeft,
21
+ paddingRight,
22
+ paddingTop,
23
+ withFooter,
24
+ contentAlignItem: alignItem,
25
+ contentFlexGrow: flexGrow,
26
+ contentFlexShrink: flexShrink,
27
+ contentJustifyContent: justifyContent
28
+ }) => ({
29
+ backgroundColor,
30
+ // We need to make sure to have sharp corners on the bottom
31
+ // if the card has a footer
32
+ borderBottomLeftRadius: withFooter ? 0 : borderRadius,
33
+ borderBottomRightRadius: withFooter ? 0 : borderRadius,
34
+ borderTopLeftRadius: borderRadius,
35
+ borderTopRightRadius: borderRadius,
36
+ paddingBottom,
37
+ paddingLeft,
38
+ paddingRight,
39
+ paddingTop,
40
+ display: 'flex',
41
+ flexDirection: 'column',
42
+ alignItem,
43
+ flexGrow,
44
+ flexShrink,
45
+ justifyContent
46
+ })
47
+ )
48
+
49
+ /**
50
+ * Card content, applying the card tokens as per the theme used.
51
+ */
52
+ const CardContent = ({ children, flexContent, tokens, variant, withFooter = false, ...rest }) => {
53
+ const viewport = useViewport()
54
+ const themeTokens = useThemeTokens('Card', tokens, variant, { viewport })
55
+
56
+ return (
57
+ <CardContentContainer
58
+ {...themeTokens}
59
+ flexContent={flexContent}
60
+ withFooter={withFooter}
61
+ {...selectProps(rest)}
62
+ >
63
+ {children}
64
+ </CardContentContainer>
65
+ )
66
+ }
67
+
68
+ CardContent.propTypes = {
69
+ ...selectedSystemPropTypes,
70
+ /**
71
+ * Card section content.
72
+ */
73
+ children: PropTypes.node,
74
+ /**
75
+ * Card tokens.
76
+ */
77
+ tokens: getTokensPropType('Card'),
78
+ /**
79
+ * Card variant.
80
+ */
81
+ variant: variantProp.propType,
82
+ /**
83
+ * Whether the card has a footer.
84
+ */
85
+ withFooter: PropTypes.bool
86
+ }
87
+
88
+ export default CardContent
@@ -0,0 +1,70 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import {
4
+ getTokensPropType,
5
+ paddingProp,
6
+ selectSystemProps,
7
+ useThemeTokens,
8
+ useViewport,
9
+ variantProp
10
+ } from '@telus-uds/components-base'
11
+ import styled from 'styled-components'
12
+ import { htmlAttrs } from '../utils'
13
+
14
+ const [selectProps, selectedSystemPropTypes] = selectSystemProps([htmlAttrs])
15
+
16
+ const CardFooterContainer = styled.div(
17
+ ({ backgroundColor, borderRadius, paddingBottom, paddingLeft, paddingRight, paddingTop }) => ({
18
+ backgroundColor,
19
+ borderBottomLeftRadius: borderRadius,
20
+ borderBottomRightRadius: borderRadius,
21
+ // @todo circle back to the following non-standard value to
22
+ // see if it can be integrated into the palette
23
+ boxShadow: 'inset 0px 1px 3px rgba(0, 0, 0, 0.05)',
24
+ paddingBottom,
25
+ paddingLeft,
26
+ paddingRight,
27
+ paddingTop
28
+ })
29
+ )
30
+
31
+ /**
32
+ * Card footer, applying the tokens as per the theme used.
33
+ */
34
+ const CardFooter = ({ children, padding, tokens, variant, ...rest }) => {
35
+ const viewport = useViewport()
36
+ const themeTokens = useThemeTokens(
37
+ 'Card',
38
+ tokens,
39
+ { ...variant, background: 'alternative' },
40
+ { viewport }
41
+ )
42
+
43
+ return (
44
+ <CardFooterContainer {...themeTokens} {...padding} {...selectProps(rest)}>
45
+ {children}
46
+ </CardFooterContainer>
47
+ )
48
+ }
49
+
50
+ CardFooter.propTypes = {
51
+ ...selectedSystemPropTypes,
52
+ /**
53
+ * Card footer content.
54
+ */
55
+ children: PropTypes.node,
56
+ /**
57
+ * Card footer padding.
58
+ */
59
+ padding: paddingProp.propType,
60
+ /**
61
+ * Card tokens.
62
+ */
63
+ tokens: getTokensPropType('Card'),
64
+ /**
65
+ * Card variant.
66
+ */
67
+ variant: variantProp.propType
68
+ }
69
+
70
+ export default CardFooter
@@ -0,0 +1,3 @@
1
+ import Card from './Card'
2
+
3
+ export default Card
@@ -0,0 +1,144 @@
1
+ /* eslint-disable react/require-default-props */
2
+ import React, { forwardRef } from 'react'
3
+ import PropTypes from 'prop-types'
4
+ import {
5
+ Spacer,
6
+ StackView,
7
+ Typography,
8
+ selectSystemProps,
9
+ useViewport,
10
+ useThemeTokens,
11
+ applyTextStyles
12
+ } from '@telus-uds/components-base'
13
+ import { viewports } from '@telus-uds/system-constants'
14
+ import styled from 'styled-components'
15
+ // Reading these from the RN palette since they will be used to generate
16
+ // the `Typography` tokens
17
+ import { htmlAttrs, transformGradient } from '../utils'
18
+ import Segment from './Segment'
19
+ import useCountdown from './useCountdown'
20
+ import { countdownVariantPropType, dictionaryContentShape } from './types'
21
+
22
+ const [selectProps, selectedSystemPropTypes] = selectSystemProps([htmlAttrs])
23
+
24
+ const Container = styled.div(({ variant: { feature, inverse, large }, themeTokens, gradient }) => ({
25
+ ...(large || (feature && { display: 'flex', flex: 0 })),
26
+ ...(feature && {
27
+ borderRadius: themeTokens.containerBorderRadius,
28
+ justifyContent: 'center',
29
+ paddingBottom: `${themeTokens.containerPaddingBottomTop}px`,
30
+ paddingLeft: `${themeTokens.containerPaddingLeftRight}px`,
31
+ paddingRight: `${themeTokens.containerPaddingLeftRight}px`,
32
+ paddingTop: `${themeTokens.containerPaddingBottomTop}px`,
33
+ width: 'fit-content'
34
+ }),
35
+ ...(feature &&
36
+ !inverse && {
37
+ background: `linear-gradient(#fff 0 0) padding-box, ${gradient} border-box`,
38
+ border: `${themeTokens.containerInverseBorder}px solid transparent`
39
+ }),
40
+ ...(feature &&
41
+ inverse && {
42
+ border: `${themeTokens.containerInverseBorder}px solid ${themeTokens.inverseBorderColor}`
43
+ })
44
+ }))
45
+
46
+ const getLabelTokens = (themeTokens) => ({
47
+ color: themeTokens.labelBorderColor,
48
+ fontWeight: themeTokens.textTimerFontWeight,
49
+ fontSize: `${themeTokens.labelFontSize}`,
50
+ lineHeight: `${themeTokens.labelLineHeight}`
51
+ })
52
+
53
+ const getMainTextTokens = (themeTokens) => ({
54
+ color: themeTokens.labelBorderColor,
55
+ fontWeight: themeTokens.textTimerFontWeight,
56
+ fontSize: themeTokens.textFontSize,
57
+ lineHeight: themeTokens.textLineHeight
58
+ })
59
+
60
+ const Countdown = forwardRef(({ copy = 'en', targetTime, tokens, variant = {}, ...rest }, ref) => {
61
+ const [days, hours, minutes, seconds] = useCountdown(targetTime)
62
+ const viewport = useViewport()
63
+ const { feature, large, label, noDivider } = variant
64
+ if (noDivider && !label) {
65
+ throw new Error('`noDivider` variant can only be used with `label` enabled!')
66
+ }
67
+ const themeTokens = useThemeTokens('Countdown', tokens, variant, { viewport })
68
+ const segmentFontSize = themeTokens.textFontSize
69
+ const semanticGradient =
70
+ themeTokens.containerGradient && transformGradient(themeTokens.containerGradient)
71
+
72
+ const mainTextTokens = getMainTextTokens(themeTokens)
73
+
74
+ const divider =
75
+ noDivider === true ? (
76
+ // StackView-based subcontainer adds a 1-step space on the each side of the divider
77
+ <Spacer direction="row" space={(feature || large) && viewport !== viewports.xs ? 7 : 2} />
78
+ ) : (
79
+ <Typography tokens={mainTextTokens}>:</Typography>
80
+ )
81
+ const labelTokens = getLabelTokens(themeTokens)
82
+ const commonProps = {
83
+ copy,
84
+ labelTokens,
85
+ numberTokens: mainTextTokens,
86
+ segmentFontSize,
87
+ variant
88
+ }
89
+
90
+ return (
91
+ // Making it focusable for accessibility purposes
92
+ <Container
93
+ ref={ref}
94
+ variant={variant}
95
+ {...selectProps(rest)}
96
+ tabIndex={0}
97
+ themeTokens={themeTokens}
98
+ gradient={semanticGradient}
99
+ >
100
+ <StackView direction="row" space={1}>
101
+ <Segment labelKey="day" number={days} segmentWidth={String(days).length} {...commonProps} />
102
+ {divider}
103
+ <Segment
104
+ labelKey="hour"
105
+ number={hours}
106
+ {...commonProps}
107
+ {...applyTextStyles(themeTokens)}
108
+ />
109
+ {divider}
110
+ <Segment
111
+ labelKey="minute"
112
+ number={minutes}
113
+ {...commonProps}
114
+ {...applyTextStyles(themeTokens)}
115
+ />
116
+ {divider}
117
+ <Segment
118
+ labelKey="second"
119
+ number={seconds}
120
+ {...commonProps}
121
+ {...applyTextStyles(themeTokens)}
122
+ />
123
+ </StackView>
124
+ </Container>
125
+ )
126
+ })
127
+ Countdown.displayName = 'Countdown'
128
+
129
+ Countdown.propTypes = {
130
+ ...selectedSystemPropTypes,
131
+ /**
132
+ * Copy language identifier (`'en'` or `'fr'`) or a dictionary instance (an object with
133
+ * the following keys: days, day, hours, hour, minutes, minute, seconds, second)
134
+ */
135
+ copy: PropTypes.oneOfType([PropTypes.oneOf(['en', 'fr']), dictionaryContentShape]),
136
+ /**
137
+ * An instance of JavaScript `Date` object or a string parseable via `Date.parse()`
138
+ * representing a point in the future to count down to.
139
+ */
140
+ targetTime: PropTypes.instanceOf(Date),
141
+ variant: countdownVariantPropType
142
+ }
143
+
144
+ export default Countdown
@@ -0,0 +1,69 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import { StackView, Typography, useCopy } from '@telus-uds/components-base'
4
+ import styled from 'styled-components'
5
+ import dictionary from './dictionary'
6
+ import { countdownVariantPropType, dictionaryContentShape } from './types'
7
+ import { SEGMENT_WIDTH_TO_FONT_SIZE_RATIO } from './constants'
8
+
9
+ // Pads with zeros on the left if it's a single digit number
10
+ const pad = (number, segmentWidth = 2) => String(number).padStart(segmentWidth, '0')
11
+
12
+ const Container = styled.div(({ segmentFontSize, segmentWidth = 2, variant: { feature } }) => ({
13
+ justifyContent: 'space-evenly',
14
+ display: 'inline-flex',
15
+ ...(feature && {
16
+ width: `${segmentFontSize * SEGMENT_WIDTH_TO_FONT_SIZE_RATIO * segmentWidth}px`,
17
+ display: 'flex'
18
+ })
19
+ }))
20
+
21
+ // A segment of the countdown string: we need to make sure it
22
+ // keeps its width constant to prevent the whole component from
23
+ // being automatically resized while using variable size fonts
24
+ const Segment = ({
25
+ copy = 'en',
26
+ segmentFontSize,
27
+ labelKey,
28
+ labelTokens,
29
+ number,
30
+ numberTokens,
31
+ segmentWidth = 2,
32
+ variant = {}
33
+ }) => {
34
+ const getCopy = useCopy({ dictionary, copy })
35
+ const { label, large, feature } = variant
36
+
37
+ return (
38
+ <Container segmentFontSize={segmentFontSize} segmentWidth={segmentWidth} variant={variant}>
39
+ <StackView
40
+ direction={large || feature ? 'column' : 'row'}
41
+ space={large || feature ? 0 : 1}
42
+ tokens={{ alignItems: 'center' }}
43
+ >
44
+ <Typography tokens={numberTokens}>{pad(number, segmentWidth)}</Typography>
45
+ {label && (
46
+ <Typography tokens={labelTokens}>
47
+ {getCopy(number === 1 ? labelKey : `${labelKey}s`)}
48
+ </Typography>
49
+ )}
50
+ </StackView>
51
+ </Container>
52
+ )
53
+ }
54
+
55
+ Segment.propTypes = {
56
+ /**
57
+ * Copy language identifier or a dictionary instance.
58
+ */
59
+ copy: PropTypes.oneOfType([PropTypes.oneOf(['en', 'fr']), dictionaryContentShape]),
60
+ segmentFontSize: PropTypes.number,
61
+ labelKey: PropTypes.oneOf(['day', 'hour', 'minute', 'second']),
62
+ labelTokens: PropTypes.object,
63
+ number: PropTypes.number,
64
+ numberTokens: PropTypes.object,
65
+ segmentWidth: PropTypes.number,
66
+ variant: countdownVariantPropType
67
+ }
68
+
69
+ export default Segment
@@ -0,0 +1,4 @@
1
+ export const DEFAULT_FONT_SIZE = 16
2
+ export const LARGE_FONT_SIZE = 64
3
+ export const SEGMENT_WIDTH_TO_FONT_SIZE_RATIO = 0.8
4
+ export const XS_FONT_SIZE = 28
@@ -0,0 +1,22 @@
1
+ export default {
2
+ en: {
3
+ days: 'Days',
4
+ day: 'Day',
5
+ hours: 'Hours',
6
+ hour: 'Hour',
7
+ minutes: 'Minutes',
8
+ minute: 'Minute',
9
+ seconds: 'Seconds',
10
+ second: 'Second'
11
+ },
12
+ fr: {
13
+ days: 'Jours',
14
+ day: 'Jour',
15
+ hours: 'Heures',
16
+ hour: 'Heure',
17
+ minutes: 'Minutes',
18
+ minute: 'Minute',
19
+ seconds: 'Secondes',
20
+ second: 'Seconde'
21
+ }
22
+ }
@@ -0,0 +1,3 @@
1
+ import Countdown from './Countdown'
2
+
3
+ export default Countdown
@@ -0,0 +1,23 @@
1
+ import PropTypes from 'prop-types'
2
+
3
+ export const countdownVariantPropType = PropTypes.shape({
4
+ feature: PropTypes.bool,
5
+ inverse: PropTypes.bool,
6
+ label: PropTypes.bool,
7
+ large: PropTypes.bool,
8
+ noDivider: PropTypes.bool
9
+ })
10
+
11
+ // If a language dictionary entry is provided, it must contain every key
12
+ export const dictionaryContentShape = PropTypes.shape({
13
+ days: PropTypes.string.isRequired,
14
+ day: PropTypes.string.isRequired,
15
+ hours: PropTypes.string.isRequired,
16
+ hour: PropTypes.string.isRequired,
17
+ minutes: PropTypes.string.isRequired,
18
+ minute: PropTypes.string.isRequired,
19
+ seconds: PropTypes.string.isRequired,
20
+ second: PropTypes.string.isRequired
21
+ })
22
+
23
+ export default { countdownVariantPropType, dictionaryContentShape }
@@ -0,0 +1,34 @@
1
+ import { useEffect, useState } from 'react'
2
+
3
+ const getTimeCounts = (countdown) => {
4
+ if (countdown <= 0) {
5
+ return [0, 0, 0, 0]
6
+ }
7
+ const days = Math.floor(countdown / (1000 * 60 * 60 * 24))
8
+ const hours = Math.floor((countdown % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
9
+ const minutes = Math.floor((countdown % (1000 * 60 * 60)) / (1000 * 60))
10
+ const seconds = Math.floor((countdown % (1000 * 60)) / 1000)
11
+
12
+ return [days, hours, minutes, seconds]
13
+ }
14
+
15
+ const useCountdown = (targetTime) => {
16
+ const countdownTime = new Date(targetTime).getTime()
17
+ if (!countdownTime) {
18
+ throw new Error('Invalid target time is provided!')
19
+ }
20
+
21
+ const [countdown, setCountdown] = useState(countdownTime - new Date().getTime())
22
+
23
+ useEffect(() => {
24
+ const interval = setInterval(() => {
25
+ setCountdown(countdownTime - new Date().getTime())
26
+ }, 1000)
27
+
28
+ return () => clearInterval(interval)
29
+ }, [countdownTime])
30
+
31
+ return getTimeCounts(countdown)
32
+ }
33
+
34
+ export default useCountdown
@@ -15,7 +15,7 @@ import {
15
15
  import Close from '../../__fixtures__/icons/Close'
16
16
  import OrderedListBase from '../OrderedList/OrderedListBase'
17
17
  import { htmlAttrs, media, renderStructuredContent } from '../utils'
18
- import dictionary from './dictionary'
18
+ import defaultDictionary from './dictionary'
19
19
 
20
20
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([htmlAttrs])
21
21
 
@@ -154,6 +154,10 @@ const ContentContainer = styled.div(
154
154
  })
155
155
  )
156
156
 
157
+ const StyledCustomContentContainer = styled.div(({ color }) => ({
158
+ color,
159
+ fontFamily: 'HelveticaNow400normal'
160
+ }))
157
161
  const usePrevious = (value) => {
158
162
  const ref = useRef()
159
163
  useEffect(() => {
@@ -184,7 +188,17 @@ const usePrevious = (value) => {
184
188
  * - When `Footnote` is closed, focus must return to the initiating element
185
189
  */
186
190
  const Footnote = (props) => {
187
- const { copy, number, content, onClose, isOpen, tokens, variant = {}, ...rest } = props
191
+ const {
192
+ copy,
193
+ number,
194
+ content,
195
+ onClose,
196
+ isOpen,
197
+ tokens,
198
+ variant = {},
199
+ dictionary = defaultDictionary,
200
+ ...rest
201
+ } = props
188
202
  const {
189
203
  footnoteBackground,
190
204
  footnoteBorderTopSizeMd,
@@ -216,7 +230,7 @@ const Footnote = (props) => {
216
230
  const footnoteRef = useRef(null)
217
231
  const headerRef = useRef(null)
218
232
  const bodyRef = useRef(null)
219
- const listRef = useRef(null)
233
+ const contentRef = useRef(null)
220
234
  const headingRef = useRef(null)
221
235
  const [data, setData] = useState({ content: null, number: null })
222
236
  const [headerHeight, setHeaderHeight] = useState('auto')
@@ -265,7 +279,7 @@ const Footnote = (props) => {
265
279
  )
266
280
 
267
281
  const saveCurrentHeight = () => {
268
- const oldHeight = listRef.current.offsetHeight
282
+ const oldHeight = contentRef.current.offsetHeight
269
283
  setBodyHeight(oldHeight)
270
284
  }
271
285
 
@@ -287,14 +301,14 @@ const Footnote = (props) => {
287
301
  event.persist()
288
302
  if (event.propertyName === 'opacity' && !isTextVisible) {
289
303
  setData({ content, number })
290
- if (bodyHeight !== listRef.current.offsetHeight) {
304
+ if (bodyHeight !== contentRef.current.offsetHeight) {
291
305
  // Set new height
292
- setBodyHeight(listRef.current.offsetHeight)
306
+ setBodyHeight(contentRef.current.offsetHeight)
293
307
  } else {
294
308
  setIsTextVisible(true)
295
309
  }
296
310
  } else {
297
- setBodyHeight(listRef.current.offsetHeight)
311
+ setBodyHeight(contentRef.current.offsetHeight)
298
312
  }
299
313
 
300
314
  if (event.propertyName === 'height' && !isTextVisible) {
@@ -359,6 +373,41 @@ const Footnote = (props) => {
359
373
  // Reset footnote on close
360
374
  useEffect(resetFootnote, [isOpen])
361
375
 
376
+ const getFootnoteBodyContent = useCallback(() => {
377
+ if (!data.number || !data.content) {
378
+ return null
379
+ }
380
+ if (React.isValidElement(data.content)) {
381
+ return (
382
+ <StyledCustomContentContainer ref={contentRef}>{data.content}</StyledCustomContentContainer>
383
+ )
384
+ }
385
+ return (
386
+ <List start={data.number} ref={contentRef} listPaddingLeft={listPaddingLeft}>
387
+ <ListItem
388
+ listItemMarkerFontSize={listItemMarkerFontSize}
389
+ listItemMarkerLineHeight={listItemMarkerLineHeight}
390
+ listItemColor={listItemColor}
391
+ listItemFontSize={listItemFontSize}
392
+ listItemLineHeight={listItemLineHeight}
393
+ listItemPaddingLeft={listItemPaddingLeft}
394
+ >
395
+ <Typography>{renderStructuredContent(data.content)}</Typography>
396
+ </ListItem>
397
+ </List>
398
+ )
399
+ }, [
400
+ data.content,
401
+ data.number,
402
+ listItemColor,
403
+ listItemFontSize,
404
+ listItemLineHeight,
405
+ listItemMarkerFontSize,
406
+ listItemMarkerLineHeight,
407
+ listItemPaddingLeft,
408
+ listPaddingLeft
409
+ ])
410
+
362
411
  return (
363
412
  <Portal>
364
413
  <div {...selectProps(rest)}>
@@ -402,20 +451,7 @@ const Footnote = (props) => {
402
451
  footnoteBodyBackground={footnoteBodyBackground}
403
452
  footnoteBodyPadding={`${footnoteBodyPaddingTop}px ${footnoteBodyPaddingRight}px ${footnoteBodyPaddingBottom}px ${footnoteBodyPaddingLeft}px`}
404
453
  >
405
- {data.number && data.content && (
406
- <List start={data.number} ref={listRef} listPaddingLeft={listPaddingLeft}>
407
- <ListItem
408
- listItemMarkerFontSize={listItemMarkerFontSize}
409
- listItemMarkerLineHeight={listItemMarkerLineHeight}
410
- listItemColor={listItemColor}
411
- listItemFontSize={listItemFontSize}
412
- listItemLineHeight={listItemLineHeight}
413
- listItemPaddingLeft={listItemPaddingLeft}
414
- >
415
- <Typography>{renderStructuredContent(data.content)}</Typography>
416
- </ListItem>
417
- </List>
418
- )}
454
+ {getFootnoteBodyContent()}
419
455
  </StyledFootnoteBody>
420
456
  </ContentContainer>
421
457
  </StyledFootnote>
@@ -429,12 +465,19 @@ const copyShape = PropTypes.shape({
429
465
  heading: PropTypes.string.isRequired
430
466
  })
431
467
 
468
+ // If a language dictionary entry is provided, it must contain every key
469
+ const dictionaryContentShape = PropTypes.shape({
470
+ a11yLabel: PropTypes.string.isRequired,
471
+ close: PropTypes.string.isRequired,
472
+ heading: PropTypes.string.isRequired
473
+ })
474
+
432
475
  Footnote.propTypes = {
433
476
  ...selectedSystemPropTypes,
434
477
  /**
435
478
  * The content.
436
479
  */
437
- content: PropTypes.string,
480
+ content: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
438
481
  /**
439
482
  * Use the `copy` prop to either select provided English or French copy by passing 'en' or 'fr' respectively.
440
483
  * To provide your own, pass a JSON object with the keys `heading` and `close`.
@@ -455,7 +498,14 @@ Footnote.propTypes = {
455
498
  * @param {Object} options Custom options
456
499
  * @param {boolean} options.returnFocus Should the `Footnote` return focus on close
457
500
  */
458
- onClose: PropTypes.func.isRequired
501
+ onClose: PropTypes.func.isRequired,
502
+ /**
503
+ * Override the default dictionary, by passing the complete dictionary object for `en` and `fr`
504
+ */
505
+ dictionary: PropTypes.shape({
506
+ en: dictionaryContentShape,
507
+ fr: dictionaryContentShape
508
+ })
459
509
  }
460
510
 
461
511
  Footnote.defaultProps = {
@@ -28,6 +28,10 @@ const StyledSubHeading = styled.div`
28
28
  margin-top: ${({ marginTop }) => marginTop}px;
29
29
  `
30
30
 
31
+ const StyledTextButtonContainer = styled.div({
32
+ display: 'flex'
33
+ })
34
+
31
35
  const StyledFooter = styled.footer(
32
36
  ({
33
37
  hasBorder,
@@ -131,13 +135,13 @@ const ModalContent = ({
131
135
  {confirmButtonText}
132
136
  </Button>
133
137
  )}
134
- <div>
135
- {hasCancelButton && (
138
+ {hasCancelButton ? (
139
+ <StyledTextButtonContainer>
136
140
  <CancelButton tokens={{ color: cancelButtonColor }} onPress={onCancel}>
137
141
  {cancelButtonText}
138
142
  </CancelButton>
139
- )}
140
- </div>
143
+ </StyledTextButtonContainer>
144
+ ) : null}
141
145
  </StyledFooter>
142
146
  )}
143
147
  </StyledModalContent>