@telus-uds/components-web 1.2.0 → 1.4.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 (140) hide show
  1. package/CHANGELOG.md +34 -2
  2. package/lib/Breadcrumbs/Breadcrumbs.js +247 -0
  3. package/lib/Breadcrumbs/Item/Item.js +165 -0
  4. package/lib/Breadcrumbs/index.js +15 -0
  5. package/lib/Callout/Callout.js +121 -0
  6. package/lib/Callout/index.js +13 -0
  7. package/lib/DatePicker/CalendarContainer.js +221 -0
  8. package/lib/DatePicker/DatePicker.js +329 -0
  9. package/lib/DatePicker/dictionary.js +134 -0
  10. package/lib/DatePicker/index.js +13 -0
  11. package/lib/DatePicker/reactDatesCss.js +12 -0
  12. package/lib/ExpandCollapseMini/ExpandCollapseMini.js +75 -0
  13. package/lib/ExpandCollapseMini/ExpandCollapseMiniControl.js +95 -0
  14. package/lib/ExpandCollapseMini/index.js +13 -0
  15. package/lib/Footnote/Footnote.js +571 -0
  16. package/lib/Footnote/FootnoteLink.js +149 -0
  17. package/lib/Footnote/dictionary.js +19 -0
  18. package/lib/Footnote/index.js +16 -0
  19. package/lib/OrderedList/Item.js +162 -0
  20. package/lib/OrderedList/ItemBase.js +42 -0
  21. package/lib/OrderedList/OrderedList.js +94 -0
  22. package/lib/OrderedList/OrderedListBase.js +68 -0
  23. package/lib/OrderedList/constants.js +9 -0
  24. package/lib/OrderedList/index.js +16 -0
  25. package/lib/PreviewCard/AuthorDate.js +64 -0
  26. package/lib/PreviewCard/PreviewCard.js +236 -0
  27. package/lib/PreviewCard/index.js +13 -0
  28. package/lib/PriceLockup/PriceLockup.js +237 -0
  29. package/lib/PriceLockup/index.js +13 -0
  30. package/lib/PriceLockup/tokens.js +131 -0
  31. package/lib/ResponsiveImage/ResponsiveImage.js +115 -0
  32. package/lib/ResponsiveImage/index.js +13 -0
  33. package/lib/Ribbon/Ribbon.js +0 -1
  34. package/lib/Span/Span.js +88 -0
  35. package/lib/Span/index.js +13 -0
  36. package/lib/index.js +91 -1
  37. package/lib/shared/FullBleedContent/FullBleedContent.js +121 -0
  38. package/lib/shared/FullBleedContent/getFullBleedBorderRadius.js +73 -0
  39. package/lib/shared/FullBleedContent/index.js +29 -0
  40. package/lib/shared/FullBleedContent/useFullBleedContentProps.js +73 -0
  41. package/lib/utils/index.js +32 -0
  42. package/lib/utils/logger.js +31 -0
  43. package/lib/utils/media.js +54 -0
  44. package/lib/utils/renderStructuredContent.js +89 -0
  45. package/lib/utils/useTypographyTheme.js +32 -0
  46. package/lib-module/Breadcrumbs/Breadcrumbs.js +228 -0
  47. package/lib-module/Breadcrumbs/Item/Item.js +141 -0
  48. package/lib-module/Breadcrumbs/index.js +1 -0
  49. package/lib-module/Callout/Callout.js +106 -0
  50. package/lib-module/Callout/index.js +2 -0
  51. package/lib-module/DatePicker/CalendarContainer.js +208 -0
  52. package/lib-module/DatePicker/DatePicker.js +302 -0
  53. package/lib-module/DatePicker/dictionary.js +127 -0
  54. package/lib-module/DatePicker/index.js +2 -0
  55. package/lib-module/DatePicker/reactDatesCss.js +3 -0
  56. package/lib-module/ExpandCollapseMini/ExpandCollapseMini.js +56 -0
  57. package/lib-module/ExpandCollapseMini/ExpandCollapseMiniControl.js +80 -0
  58. package/lib-module/ExpandCollapseMini/index.js +2 -0
  59. package/lib-module/Footnote/Footnote.js +541 -0
  60. package/lib-module/Footnote/FootnoteLink.js +130 -0
  61. package/lib-module/Footnote/dictionary.js +12 -0
  62. package/lib-module/Footnote/index.js +4 -0
  63. package/lib-module/OrderedList/Item.js +139 -0
  64. package/lib-module/OrderedList/ItemBase.js +28 -0
  65. package/lib-module/OrderedList/OrderedList.js +71 -0
  66. package/lib-module/OrderedList/OrderedListBase.js +48 -0
  67. package/lib-module/OrderedList/constants.js +2 -0
  68. package/lib-module/OrderedList/index.js +4 -0
  69. package/lib-module/PreviewCard/AuthorDate.js +53 -0
  70. package/lib-module/PreviewCard/PreviewCard.js +211 -0
  71. package/lib-module/PreviewCard/index.js +2 -0
  72. package/lib-module/PriceLockup/PriceLockup.js +213 -0
  73. package/lib-module/PriceLockup/index.js +2 -0
  74. package/lib-module/PriceLockup/tokens.js +120 -0
  75. package/lib-module/ResponsiveImage/ResponsiveImage.js +100 -0
  76. package/lib-module/ResponsiveImage/index.js +2 -0
  77. package/lib-module/Ribbon/Ribbon.js +1 -2
  78. package/lib-module/Span/Span.js +70 -0
  79. package/lib-module/Span/index.js +2 -0
  80. package/lib-module/index.js +10 -0
  81. package/lib-module/shared/FullBleedContent/FullBleedContent.js +106 -0
  82. package/lib-module/shared/FullBleedContent/getFullBleedBorderRadius.js +65 -0
  83. package/lib-module/shared/FullBleedContent/index.js +4 -0
  84. package/lib-module/shared/FullBleedContent/useFullBleedContentProps.js +65 -0
  85. package/lib-module/utils/index.js +5 -1
  86. package/lib-module/utils/logger.js +18 -0
  87. package/lib-module/utils/media.js +46 -0
  88. package/lib-module/utils/renderStructuredContent.js +77 -0
  89. package/lib-module/utils/useTypographyTheme.js +24 -0
  90. package/package.json +9 -4
  91. package/src/Breadcrumbs/Breadcrumbs.jsx +222 -0
  92. package/src/Breadcrumbs/Item/Item.jsx +127 -0
  93. package/src/Breadcrumbs/index.js +1 -0
  94. package/src/Callout/Callout.jsx +76 -0
  95. package/src/Callout/index.js +3 -0
  96. package/src/DatePicker/CalendarContainer.jsx +210 -0
  97. package/src/DatePicker/DatePicker.jsx +303 -0
  98. package/src/DatePicker/dictionary.js +92 -0
  99. package/src/DatePicker/index.js +3 -0
  100. package/src/DatePicker/reactDatesCss.js +892 -0
  101. package/src/ExpandCollapseMini/ExpandCollapseMini.jsx +48 -0
  102. package/src/ExpandCollapseMini/ExpandCollapseMiniControl.jsx +67 -0
  103. package/src/ExpandCollapseMini/index.js +3 -0
  104. package/src/Footnote/Footnote.jsx +468 -0
  105. package/src/Footnote/FootnoteLink.jsx +120 -0
  106. package/src/Footnote/dictionary.js +12 -0
  107. package/src/Footnote/index.js +6 -0
  108. package/src/OrderedList/Item.jsx +121 -0
  109. package/src/OrderedList/ItemBase.jsx +18 -0
  110. package/src/OrderedList/OrderedList.jsx +61 -0
  111. package/src/OrderedList/OrderedListBase.jsx +38 -0
  112. package/src/OrderedList/constants.js +2 -0
  113. package/src/OrderedList/index.js +6 -0
  114. package/src/PreviewCard/AuthorDate.jsx +31 -0
  115. package/src/PreviewCard/PreviewCard.jsx +201 -0
  116. package/src/PreviewCard/index.js +3 -0
  117. package/src/PriceLockup/PriceLockup.jsx +210 -0
  118. package/src/PriceLockup/index.js +3 -0
  119. package/src/PriceLockup/tokens.js +58 -0
  120. package/src/ResponsiveImage/ResponsiveImage.jsx +77 -0
  121. package/src/ResponsiveImage/index.js +3 -0
  122. package/src/Ribbon/Ribbon.jsx +0 -1
  123. package/src/Span/Span.jsx +66 -0
  124. package/src/Span/index.js +3 -0
  125. package/src/index.js +10 -0
  126. package/src/shared/FullBleedContent/FullBleedContent.jsx +90 -0
  127. package/src/shared/FullBleedContent/getFullBleedBorderRadius.js +55 -0
  128. package/src/shared/FullBleedContent/index.js +6 -0
  129. package/src/shared/FullBleedContent/useFullBleedContentProps.js +63 -0
  130. package/src/utils/index.js +5 -1
  131. package/src/utils/logger.js +20 -0
  132. package/src/utils/media.js +40 -0
  133. package/src/utils/renderStructuredContent.jsx +73 -0
  134. package/src/utils/useTypographyTheme.js +14 -0
  135. package/types/Callout.d.ts +13 -0
  136. package/types/DatePicker.d.ts +21 -0
  137. package/types/Footnote.d.ts +21 -0
  138. package/types/FootnoteLink.d.ts +20 -0
  139. package/types/PriceLockup.d.ts +22 -0
  140. package/types/common.d.ts +14 -0
@@ -0,0 +1,48 @@
1
+ import React, { forwardRef } from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import { ExpandCollapse, useThemeTokens } from '@telus-uds/components-base'
4
+ import ExpandCollapseMiniControl from './ExpandCollapseMiniControl'
5
+
6
+ const ExpandCollapseMini = forwardRef(({ children, onToggle, tokens, ...rest }, ref) => {
7
+ const { variant } = rest
8
+ const { borderWidth } = useThemeTokens('ExpandCollapseMini', tokens, variant)
9
+
10
+ const handleChange = (openPanels, event) => {
11
+ if (typeof onToggle === 'function') {
12
+ const isOpen = openPanels.length > 0
13
+ onToggle(event, isOpen)
14
+ }
15
+ }
16
+
17
+ return (
18
+ <ExpandCollapse tokens={{ borderWidth }} onChange={handleChange}>
19
+ {(expandProps) => (
20
+ <ExpandCollapse.Panel
21
+ {...expandProps}
22
+ panelId="ExpandCollapseMiniPanel"
23
+ controlTokens={{ icon: null }}
24
+ // TODO refactor
25
+ // eslint-disable-next-line react/no-unstable-nested-components
26
+ control={(pressableState) => (
27
+ <ExpandCollapseMiniControl pressableState={pressableState} {...rest} />
28
+ )}
29
+ controlRef={ref}
30
+ >
31
+ {children}
32
+ </ExpandCollapse.Panel>
33
+ )}
34
+ </ExpandCollapse>
35
+ )
36
+ })
37
+ ExpandCollapseMini.displayName = 'ExpandCollapseMini'
38
+
39
+ ExpandCollapseMini.propTypes = {
40
+ ...ExpandCollapseMiniControl.propTypes,
41
+ onToggle: PropTypes.func,
42
+ children: PropTypes.node.isRequired
43
+ }
44
+ ExpandCollapseMini.defaultProps = {
45
+ onToggle: () => {}
46
+ }
47
+
48
+ export default ExpandCollapseMini
@@ -0,0 +1,67 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import { selectSystemProps, Link, useThemeTokens } from '@telus-uds/components-base'
4
+ import { htmlAttrs } from '../utils'
5
+
6
+ const [selectProps, selectedSystemPropTypes] = selectSystemProps([htmlAttrs])
7
+
8
+ // The ExpandCollapseControl has all the appropriate role, a11y, press handling etc
9
+ // and a more appropriate press area, defer interaction handling to it.
10
+ const presentationOnly = {
11
+ accessibilityRole: null, // Treat as regular flow content with the Control
12
+ pointerEvents: 'none', // Stop RNW from stopping clicks from bubbling to Control
13
+ focusable: false // Stop RNW from setting tabIndex={0}: focus goes to Control only
14
+ }
15
+
16
+ const ExpandCollapseMiniControl = ({
17
+ pressableState,
18
+ collapseTitle,
19
+ expandTitle = collapseTitle,
20
+ iconPosition = 'right',
21
+ tokens,
22
+ variant = {},
23
+ ...rest
24
+ }) => {
25
+ const { expanded, hover } = pressableState || {}
26
+
27
+ const { size, icon } = useThemeTokens('ExpandCollapseMiniControl', tokens, variant, { expanded })
28
+
29
+ // Choose hover styles when any part of Control is hoverred
30
+ const appearance = { ...variant, hover }
31
+
32
+ const getTokens = (linkState) => {
33
+ const { hover: linkHover } = linkState || {}
34
+ const isHovered = hover || linkHover
35
+ if (isHovered) {
36
+ // Include vertical icon animation on hover alongside built-in Link theme, the size is size4
37
+ return { iconTranslateY: (expanded ? -1 : 1) * size }
38
+ }
39
+
40
+ return {}
41
+ }
42
+
43
+ return (
44
+ <Link
45
+ variant={appearance}
46
+ icon={icon}
47
+ iconPosition={iconPosition}
48
+ tokens={(linkState) => ({ ...getTokens(linkState), outerBorderWidth: 0 })}
49
+ {...presentationOnly}
50
+ {...selectProps(rest)}
51
+ >
52
+ {expanded ? expandTitle : collapseTitle}
53
+ </Link>
54
+ )
55
+ }
56
+
57
+ ExpandCollapseMiniControl.propTypes = {
58
+ ...selectedSystemPropTypes,
59
+ ...Link.propTypes,
60
+ onPress: PropTypes.func,
61
+ expandTitle: PropTypes.string.isRequired,
62
+ collapseTitle: PropTypes.string.isRequired,
63
+ pressableState: PropTypes.object,
64
+ variant: PropTypes.object
65
+ }
66
+
67
+ export default ExpandCollapseMiniControl
@@ -0,0 +1,3 @@
1
+ import ExpandCollapseMini from './ExpandCollapseMini'
2
+
3
+ export default ExpandCollapseMini
@@ -0,0 +1,468 @@
1
+ import React, { useRef, useEffect, useState, useCallback } from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import styled, { createGlobalStyle } from 'styled-components'
4
+ import {
5
+ Icon,
6
+ Portal,
7
+ selectSystemProps,
8
+ Typography,
9
+ useCopy,
10
+ useTheme,
11
+ useResponsiveProp,
12
+ useThemeTokens
13
+ } from '@telus-uds/components-base'
14
+
15
+ import Close from '../../__fixtures__/icons/Close'
16
+ import OrderedListBase from '../OrderedList/OrderedListBase'
17
+ import { htmlAttrs, media, renderStructuredContent } from '../utils'
18
+ import dictionary from './dictionary'
19
+
20
+ const [selectProps, selectedSystemPropTypes] = selectSystemProps([htmlAttrs])
21
+
22
+ const GlobalBodyScrollLock = createGlobalStyle({
23
+ 'html, body': media().until('md').css({ overflow: 'hidden' })
24
+ })
25
+
26
+ const StyledFootnote = styled.div(
27
+ ({ footnoteBackground, isVisible, footnoteBorderTop }) => ({
28
+ position: 'fixed',
29
+ overflowY: 'scroll',
30
+ top: 0,
31
+ left: 0,
32
+ height: '100vh',
33
+ width: '100vw',
34
+ backgroundColor: footnoteBackground,
35
+ display: 'block',
36
+ boxShadow: '0 0 16px 0 rgba(0, 0, 0, 0.1)',
37
+ transform: 'translateY(100%)',
38
+ transition: 'transform 500ms ease-out',
39
+ '@media() (prefers-reduced-motion: reduce)': {
40
+ transition: 'none'
41
+ },
42
+ zIndex: 99999,
43
+ visibility: isVisible ? 'visible' : 'hidden',
44
+ ...media().from('md').css({
45
+ top: 'auto',
46
+ bottom: 0,
47
+ height: 'auto',
48
+ maxHeight: '50vh',
49
+ borderTop: footnoteBorderTop
50
+ })
51
+ }),
52
+ ({ isOpen }) => {
53
+ if (isOpen) {
54
+ return {
55
+ transform: 'translateY(0)'
56
+ }
57
+ }
58
+ return {}
59
+ }
60
+ )
61
+
62
+ const StyledFootnoteHeader = styled.div({
63
+ position: 'relative',
64
+ width: '100%'
65
+ })
66
+
67
+ const StyledHeader = styled.div(({ headerMargin }) => ({
68
+ alignItems: 'center',
69
+ display: 'flex',
70
+ flexDirection: 'row',
71
+ justifyContent: 'space-between',
72
+ margin: headerMargin
73
+ }))
74
+
75
+ const StyledFootnoteBody = styled.div(
76
+ {
77
+ overflow: 'auto',
78
+ transition: 'height 300ms ease-out, opacity 200ms ease-out',
79
+ transform: 'translateZ(0)',
80
+ '@media() (prefers-reduced-motion: reduce)': {
81
+ transition: 'height 1ms ease-out, opacity 1ms ease-out'
82
+ }
83
+ },
84
+ ({ footnoteBodyBackground, footnoteBodyPadding }) => ({
85
+ backgroundColor: footnoteBodyBackground,
86
+ padding: footnoteBodyPadding
87
+ }),
88
+ ({ headerHeight }) => ({
89
+ maxHeight: `calc(100vh - ${headerHeight}px)`,
90
+ ...media()
91
+ .from('md')
92
+ .css({
93
+ maxHeight: `calc(50vh - ${headerHeight}px)`
94
+ })
95
+ }),
96
+ ({ bodyHeight, isTextVisible }) => ({
97
+ height: bodyHeight,
98
+ opacity: isTextVisible ? 1 : 0
99
+ })
100
+ )
101
+
102
+ const List = styled(OrderedListBase)(({ listPaddingLeft }) => ({
103
+ listStylePosition: 'outside',
104
+ paddingLeft: listPaddingLeft
105
+ }))
106
+
107
+ const ListItem = styled(OrderedListBase.Item)(
108
+ ({
109
+ listItemMarkerFontSize,
110
+ listItemMarkerLineHeight,
111
+ listItemColor,
112
+ listItemFontSize,
113
+ listItemLineHeight,
114
+ listItemPaddingLeft
115
+ }) => ({
116
+ display: 'list-item',
117
+ '&::marker': {
118
+ fontFamily: 'HelveticaNow400normal',
119
+ fontSize: listItemMarkerFontSize,
120
+ lineHeight: listItemMarkerLineHeight,
121
+ textAlign: 'end !important'
122
+ },
123
+ color: listItemColor,
124
+ fontFamily: 'HelveticaNow400normal',
125
+ fontSize: listItemFontSize,
126
+ lineHeight: listItemLineHeight,
127
+ paddingLeft: listItemPaddingLeft
128
+ })
129
+ )
130
+
131
+ const CloseButton = styled.button(
132
+ ({ closeButtonBorder, closeButtonHeight, closeButtonMargin, closeButtonWidth }) => ({
133
+ alignItems: 'center',
134
+ borderRadius: '50%',
135
+ cursor: 'pointer',
136
+ display: 'flex',
137
+ justifyContent: 'center',
138
+ border: closeButtonBorder,
139
+ height: closeButtonHeight,
140
+ margin: closeButtonMargin,
141
+ width: closeButtonWidth
142
+ })
143
+ )
144
+
145
+ const ContentContainer = styled.div(
146
+ {
147
+ 'margin-left': 'auto',
148
+ 'margin-right': 'auto',
149
+ left: 0,
150
+ right: 0
151
+ },
152
+ ({ maxWidth }) => ({
153
+ width: maxWidth
154
+ })
155
+ )
156
+
157
+ const usePrevious = (value) => {
158
+ const ref = useRef()
159
+ useEffect(() => {
160
+ ref.current = value
161
+ })
162
+ if (ref.current) {
163
+ return ref.current
164
+ }
165
+ return {}
166
+ }
167
+
168
+ /**
169
+ * Use `Footnote` to display a single legal content.
170
+ *
171
+ * ## Usage Criteria
172
+ *
173
+ * - Use `Footnote` to display a single legal statement
174
+ * - Display on top of all UI, including other sticky elements such as Cart Summary
175
+ * - Dismiss by clicking on the close button, clicking anywhere outside of the `Footnote`, or by pressing the ESC key
176
+ * - Responsive display based on breakpoints
177
+ * - Use copy to set language, ‘en’ for English or ‘fr’ for French
178
+ *
179
+ * ## Accessibility requirements
180
+ *
181
+ * - Only one instance of `Footnote` should display at a time
182
+ * - Place `Footnote` as the last element in the body or main
183
+ * - When `Footnote` is open, the inert prop must be set on all children of body excluding the Footnote
184
+ * - When `Footnote` is closed, focus must return to the initiating element
185
+ */
186
+ const Footnote = (props) => {
187
+ const { copy, number, content, onClose, isOpen, tokens, variant = {}, ...rest } = props
188
+ const {
189
+ footnoteBackground,
190
+ footnoteBorderTopSizeMd,
191
+ footnoteBorderColorMd,
192
+ headerMargin,
193
+ footnoteBodyBackground,
194
+ footnoteBodyPaddingLeft,
195
+ footnoteBodyPaddingRight,
196
+ footnoteBodyPaddingTop,
197
+ footnoteBodyPaddingBottom,
198
+ listPaddingLeft,
199
+ listItemMarkerFontSize,
200
+ listItemMarkerLineHeight,
201
+ listItemColor,
202
+ listItemFontSize,
203
+ listItemLineHeight,
204
+ listItemPaddingLeft,
205
+ closeButtonBorderSize,
206
+ closeButtonBorderColor,
207
+ closeButtonHeight,
208
+ closeButtonMarginTop,
209
+ closeButtonMarginLeft,
210
+ closeButtonMarginRight,
211
+ closeButtonMarginBottom,
212
+ closeButtonWidth,
213
+ closeButtonIconSize
214
+ } = useThemeTokens('Footnote', tokens, variant)
215
+
216
+ const footnoteRef = useRef(null)
217
+ const headerRef = useRef(null)
218
+ const bodyRef = useRef(null)
219
+ const listRef = useRef(null)
220
+ const headingRef = useRef(null)
221
+ const [data, setData] = useState({ content: null, number: null })
222
+ const [headerHeight, setHeaderHeight] = useState('auto')
223
+ const [bodyHeight, setBodyHeight] = useState('auto')
224
+ const [isVisible, setIsVisible] = useState(false)
225
+ const [isTextVisible, setIsTextVisible] = useState(true)
226
+ const getCopy = useCopy({ dictionary, copy })
227
+
228
+ const prevProps = usePrevious(props)
229
+ const theme = useTheme()
230
+ const maxWidth = useResponsiveProp(theme.themeOptions?.contentMaxWidth)
231
+
232
+ const closeFootnote = useCallback(
233
+ (event, options) => {
234
+ onClose(event, options)
235
+ },
236
+ [onClose]
237
+ )
238
+
239
+ // Listen for ESCAPE, close button clicks, and clicks outside of the Footnote. Call onClose.
240
+ const handleClose = useCallback(
241
+ (event) => {
242
+ if (event.type === 'keydown') {
243
+ if (event.key === 'Escape' || event.key === 27) {
244
+ closeFootnote(event, { returnFocus: true })
245
+ }
246
+ } else if (
247
+ (event.type === 'click' || event.type === 'mousedown') &&
248
+ footnoteRef?.current &&
249
+ event.target &&
250
+ !footnoteRef?.current?.contains(event.target) &&
251
+ event.target.getAttribute('data-tds-id') !== 'footnote-link'
252
+ ) {
253
+ closeFootnote(event, { returnFocus: false })
254
+ } else if (
255
+ event.type === 'touchstart' &&
256
+ footnoteRef?.current &&
257
+ event.touches[0].target &&
258
+ !footnoteRef?.current?.contains(event.touches[0].target) &&
259
+ event.touches[0].target.getAttribute('data-tds-id') !== 'footnote-link'
260
+ ) {
261
+ closeFootnote(event, { returnFocus: false })
262
+ }
263
+ },
264
+ [closeFootnote]
265
+ )
266
+
267
+ const saveCurrentHeight = () => {
268
+ const oldHeight = listRef.current.offsetHeight
269
+ setBodyHeight(oldHeight)
270
+ }
271
+
272
+ const focusHeading = () => {
273
+ if (Boolean(content) && isVisible && headingRef && headingRef.current !== null) {
274
+ headingRef.current.focus()
275
+ }
276
+ }
277
+
278
+ const handleStyledFootnoteTransitionEnd = (event) => {
279
+ if (event.propertyName === 'transform' && !isOpen) {
280
+ setIsVisible(false)
281
+ } else {
282
+ focusHeading()
283
+ }
284
+ }
285
+
286
+ const handleTransitionEnd = (event) => {
287
+ event.persist()
288
+ if (event.propertyName === 'opacity' && !isTextVisible) {
289
+ setData({ content, number })
290
+ if (bodyHeight !== listRef.current.offsetHeight) {
291
+ // Set new height
292
+ setBodyHeight(listRef.current.offsetHeight)
293
+ } else {
294
+ setIsTextVisible(true)
295
+ }
296
+ } else {
297
+ setBodyHeight(listRef.current.offsetHeight)
298
+ }
299
+
300
+ if (event.propertyName === 'height' && !isTextVisible) {
301
+ setIsTextVisible(true)
302
+ }
303
+ }
304
+
305
+ const resetFootnote = () => {
306
+ // Reset footnote state if closed
307
+ if (!isOpen) {
308
+ setBodyHeight('auto')
309
+ setIsTextVisible(true)
310
+ }
311
+ }
312
+
313
+ // Set height of header on mount
314
+ useEffect(() => {
315
+ setHeaderHeight(headerRef.current?.offsetHeight)
316
+ }, [])
317
+
318
+ const preventDefault = (event) => {
319
+ if (!bodyRef.current.contains(event.touches[0].target)) {
320
+ event.preventDefault()
321
+ }
322
+ }
323
+
324
+ // Add listeners for mouse clicks outside of Footnote and for ESCAPE key presses
325
+ useEffect(() => {
326
+ if (isOpen) {
327
+ setIsVisible(true)
328
+ document.addEventListener('mousedown', handleClose)
329
+ window.addEventListener('click', handleClose)
330
+ window.addEventListener('keydown', handleClose)
331
+ window.addEventListener('touchstart', handleClose)
332
+ window.addEventListener('touchmove', preventDefault, { passive: false })
333
+ }
334
+ return () => {
335
+ if (isOpen) {
336
+ document.addEventListener('mousedown', handleClose)
337
+ window.removeEventListener('click', handleClose)
338
+ window.removeEventListener('keydown', handleClose)
339
+ window.removeEventListener('touchstart', handleClose)
340
+ window.removeEventListener('touchmove', preventDefault)
341
+ }
342
+ }
343
+ }, [handleClose, isOpen])
344
+
345
+ // Set data if opening a new footnote
346
+ useEffect(() => {
347
+ if (isOpen && !prevProps.isOpen) {
348
+ setData({ content, number })
349
+ }
350
+ }, [isOpen, prevProps.isOpen, content, number])
351
+
352
+ useEffect(() => {
353
+ if (isOpen && prevProps.isOpen && number !== prevProps.number) {
354
+ saveCurrentHeight()
355
+ setIsTextVisible(false)
356
+ }
357
+ }, [number, isOpen, prevProps.isOpen, prevProps.number])
358
+
359
+ // Reset footnote on close
360
+ useEffect(resetFootnote, [isOpen])
361
+
362
+ return (
363
+ <Portal>
364
+ <div {...selectProps(rest)}>
365
+ {isOpen && <GlobalBodyScrollLock />}
366
+ <StyledFootnote
367
+ ref={footnoteRef}
368
+ isOpen={isOpen}
369
+ isVisible={isVisible}
370
+ onTransitionEnd={handleStyledFootnoteTransitionEnd}
371
+ tabIndex={0}
372
+ footnoteBackground={footnoteBackground}
373
+ footnoteBorderTop={`${footnoteBorderTopSizeMd}px solid ${footnoteBorderColorMd}`}
374
+ >
375
+ <ContentContainer maxWidth={maxWidth}>
376
+ <StyledFootnoteHeader ref={headerRef}>
377
+ <StyledHeader ref={headingRef} headerMargin={headerMargin}>
378
+ <Typography block heading tabIndex={-1} variant={{ size: 'h4' }}>
379
+ {getCopy('heading')}
380
+ </Typography>
381
+ <CloseButton
382
+ closeButtonBorder={`${closeButtonBorderSize}px solid ${closeButtonBorderColor}`}
383
+ closeButtonWidth={`${closeButtonWidth}px`}
384
+ closeButtonHeight={`${closeButtonHeight}px`}
385
+ closeButtonMargin={`${closeButtonMarginTop}px ${closeButtonMarginRight}px ${closeButtonMarginBottom}px ${closeButtonMarginLeft}px`}
386
+ onClick={(event) => {
387
+ closeFootnote(event, { returnFocus: true })
388
+ }}
389
+ aria-label={getCopy('close')}
390
+ >
391
+ <Icon icon={Close} tokens={{ size: `${closeButtonIconSize}px` }} />
392
+ </CloseButton>
393
+ </StyledHeader>
394
+ </StyledFootnoteHeader>
395
+ <StyledFootnoteBody
396
+ ref={bodyRef}
397
+ bodyHeight={bodyHeight}
398
+ headerHeight={headerHeight}
399
+ isTextVisible={isTextVisible}
400
+ onTransitionEnd={handleTransitionEnd}
401
+ maxWidth={theme.contentMaxWidth}
402
+ footnoteBodyBackground={footnoteBodyBackground}
403
+ footnoteBodyPadding={`${footnoteBodyPaddingTop}px ${footnoteBodyPaddingRight}px ${footnoteBodyPaddingBottom}px ${footnoteBodyPaddingLeft}px`}
404
+ >
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
+ )}
419
+ </StyledFootnoteBody>
420
+ </ContentContainer>
421
+ </StyledFootnote>
422
+ </div>
423
+ </Portal>
424
+ )
425
+ }
426
+
427
+ const copyShape = PropTypes.shape({
428
+ close: PropTypes.string.isRequired,
429
+ heading: PropTypes.string.isRequired
430
+ })
431
+
432
+ Footnote.propTypes = {
433
+ ...selectedSystemPropTypes,
434
+ /**
435
+ * The content.
436
+ */
437
+ content: PropTypes.string,
438
+ /**
439
+ * Use the `copy` prop to either select provided English or French copy by passing 'en' or 'fr' respectively.
440
+ * To provide your own, pass a JSON object with the keys `heading` and `close`.
441
+ */
442
+ copy: PropTypes.oneOfType([PropTypes.oneOf(['en', 'fr']), copyShape]),
443
+ /**
444
+ * A boolean flag used hide or show the `Footnote`. Set to `true` to open the `Footnote`.
445
+ */
446
+ isOpen: PropTypes.bool,
447
+ /**
448
+ * The number, must match the number of the `FootnoteLink` that initiated the `Footnote`.
449
+ */
450
+ number: PropTypes.number,
451
+ /**
452
+ * A callback function to handle the closing of the footnote.
453
+ *
454
+ * @param {SyntheticEvent} event The React `SyntheticEvent`
455
+ * @param {Object} options Custom options
456
+ * @param {boolean} options.returnFocus Should the `Footnote` return focus on close
457
+ */
458
+ onClose: PropTypes.func.isRequired
459
+ }
460
+
461
+ Footnote.defaultProps = {
462
+ isOpen: false,
463
+ number: undefined,
464
+ content: undefined,
465
+ copy: 'en'
466
+ }
467
+
468
+ export default Footnote
@@ -0,0 +1,120 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import styled from 'styled-components'
4
+ import { selectSystemProps, useCopy, useThemeTokens } from '@telus-uds/components-base'
5
+ import dictionary from './dictionary'
6
+ import { htmlAttrs } from '../utils'
7
+
8
+ const [selectProps, selectedSystemPropTypes] = selectSystemProps([htmlAttrs])
9
+
10
+ const StyledSup = styled.sup(({ fontSize = 'smaller', lineHeight, paddingLeft, paddingRight }) => ({
11
+ border: 0,
12
+ color: 'inherit',
13
+ cursor: 'pointer',
14
+ fontSize,
15
+ lineHeight,
16
+ margin: 0,
17
+ paddingVertical: 0,
18
+ paddingLeft,
19
+ paddingRight,
20
+ textDecoration: 'underline'
21
+ }))
22
+
23
+ /**
24
+ * Use `FootnoteLink` to open `Footnote` component and display related legal content.
25
+ *
26
+ * ## Usage Criteria
27
+ *
28
+ * - Use FootnoteLink to open a Footnote component and display related legal content.
29
+ * - Avoid using FootnoteLink if there is only one annotation on a page. Consider including
30
+ * the annotation as part of the content whenever possible.
31
+ */
32
+ const FootnoteLink = ({
33
+ copy = 'en',
34
+ number = [],
35
+ onClick,
36
+ fontSize,
37
+ tokens,
38
+ variant = {},
39
+ ...rest
40
+ }) => {
41
+ const { lineHeight, paddingLeft, paddingRight } = useThemeTokens('FootnoteLink', tokens, variant)
42
+ const numbers = Array.isArray(number) ? number : [number]
43
+ const refs = numbers.map(() => React.createRef())
44
+ const handleClick = (index) => {
45
+ onClick(numbers[index], refs[index])
46
+ }
47
+ const getCopy = useCopy({ dictionary, copy })
48
+
49
+ const handleOnClick = (event, index) => {
50
+ event.preventDefault()
51
+ event.stopPropagation()
52
+ handleClick(index)
53
+ }
54
+
55
+ const handleOnKeyDown = (event, index) => {
56
+ if (event.key === 'Enter' || event.key === 13) {
57
+ handleClick(index)
58
+ }
59
+ }
60
+
61
+ return (
62
+ <>
63
+ {numbers.map((num, index) => (
64
+ <StyledSup
65
+ onKeyDown={(event) => handleOnKeyDown(event, index)}
66
+ role="button"
67
+ aria-label={getCopy('a11yLabel')}
68
+ key={num}
69
+ ref={refs[index]}
70
+ onClick={(event) => handleOnClick(event, index)}
71
+ fontSize={fontSize}
72
+ lineHeight={lineHeight}
73
+ paddingLeft={paddingLeft}
74
+ paddingRight={paddingRight}
75
+ {...selectProps(rest)}
76
+ >
77
+ {`${num}${index !== numbers.length - 1 ? ',' : ''}`}
78
+ </StyledSup>
79
+ ))}
80
+ </>
81
+ )
82
+ }
83
+
84
+ const copyShape = PropTypes.shape({
85
+ a11yLabel: PropTypes.string.isRequired
86
+ })
87
+
88
+ FootnoteLink.propTypes = {
89
+ ...selectedSystemPropTypes,
90
+ /**
91
+ * Use the `copy` prop to either select provided English or French copy by passing 'en' or 'fr' respectively.
92
+ * To provide your own, pass a JSON object with the key `a11yLabel`.
93
+ */
94
+ copy: PropTypes.oneOfType([PropTypes.oneOf(['en', 'fr']), copyShape]),
95
+ /**
96
+ * The footnote number, or multiple numbers if passed as an array.
97
+ * If using an array, a comma-separated group of numbers will be rendered as superscript.
98
+ */
99
+ number: PropTypes.oneOfType([
100
+ PropTypes.number,
101
+ PropTypes.arrayOf(PropTypes.number),
102
+ PropTypes.string,
103
+ PropTypes.arrayOf(PropTypes.string)
104
+ ]).isRequired,
105
+ /**
106
+ * A callback function to handle the click of a FootnoteLink.
107
+ */
108
+ onClick: PropTypes.func.isRequired,
109
+ /**
110
+ * Override default `fontSize` to set specific font size value
111
+ */
112
+ fontSize: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
113
+ }
114
+
115
+ FootnoteLink.defaultProps = {
116
+ copy: 'en',
117
+ fontSize: 'smaller'
118
+ }
119
+
120
+ export default FootnoteLink