@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.
- package/CHANGELOG.md +34 -2
- package/lib/Breadcrumbs/Breadcrumbs.js +247 -0
- package/lib/Breadcrumbs/Item/Item.js +165 -0
- package/lib/Breadcrumbs/index.js +15 -0
- package/lib/Callout/Callout.js +121 -0
- package/lib/Callout/index.js +13 -0
- package/lib/DatePicker/CalendarContainer.js +221 -0
- package/lib/DatePicker/DatePicker.js +329 -0
- package/lib/DatePicker/dictionary.js +134 -0
- package/lib/DatePicker/index.js +13 -0
- package/lib/DatePicker/reactDatesCss.js +12 -0
- package/lib/ExpandCollapseMini/ExpandCollapseMini.js +75 -0
- package/lib/ExpandCollapseMini/ExpandCollapseMiniControl.js +95 -0
- package/lib/ExpandCollapseMini/index.js +13 -0
- package/lib/Footnote/Footnote.js +571 -0
- package/lib/Footnote/FootnoteLink.js +149 -0
- package/lib/Footnote/dictionary.js +19 -0
- package/lib/Footnote/index.js +16 -0
- package/lib/OrderedList/Item.js +162 -0
- package/lib/OrderedList/ItemBase.js +42 -0
- package/lib/OrderedList/OrderedList.js +94 -0
- package/lib/OrderedList/OrderedListBase.js +68 -0
- package/lib/OrderedList/constants.js +9 -0
- package/lib/OrderedList/index.js +16 -0
- package/lib/PreviewCard/AuthorDate.js +64 -0
- package/lib/PreviewCard/PreviewCard.js +236 -0
- package/lib/PreviewCard/index.js +13 -0
- package/lib/PriceLockup/PriceLockup.js +237 -0
- package/lib/PriceLockup/index.js +13 -0
- package/lib/PriceLockup/tokens.js +131 -0
- package/lib/ResponsiveImage/ResponsiveImage.js +115 -0
- package/lib/ResponsiveImage/index.js +13 -0
- package/lib/Ribbon/Ribbon.js +0 -1
- package/lib/Span/Span.js +88 -0
- package/lib/Span/index.js +13 -0
- package/lib/index.js +91 -1
- package/lib/shared/FullBleedContent/FullBleedContent.js +121 -0
- package/lib/shared/FullBleedContent/getFullBleedBorderRadius.js +73 -0
- package/lib/shared/FullBleedContent/index.js +29 -0
- package/lib/shared/FullBleedContent/useFullBleedContentProps.js +73 -0
- package/lib/utils/index.js +32 -0
- package/lib/utils/logger.js +31 -0
- package/lib/utils/media.js +54 -0
- package/lib/utils/renderStructuredContent.js +89 -0
- package/lib/utils/useTypographyTheme.js +32 -0
- package/lib-module/Breadcrumbs/Breadcrumbs.js +228 -0
- package/lib-module/Breadcrumbs/Item/Item.js +141 -0
- package/lib-module/Breadcrumbs/index.js +1 -0
- package/lib-module/Callout/Callout.js +106 -0
- package/lib-module/Callout/index.js +2 -0
- package/lib-module/DatePicker/CalendarContainer.js +208 -0
- package/lib-module/DatePicker/DatePicker.js +302 -0
- package/lib-module/DatePicker/dictionary.js +127 -0
- package/lib-module/DatePicker/index.js +2 -0
- package/lib-module/DatePicker/reactDatesCss.js +3 -0
- package/lib-module/ExpandCollapseMini/ExpandCollapseMini.js +56 -0
- package/lib-module/ExpandCollapseMini/ExpandCollapseMiniControl.js +80 -0
- package/lib-module/ExpandCollapseMini/index.js +2 -0
- package/lib-module/Footnote/Footnote.js +541 -0
- package/lib-module/Footnote/FootnoteLink.js +130 -0
- package/lib-module/Footnote/dictionary.js +12 -0
- package/lib-module/Footnote/index.js +4 -0
- package/lib-module/OrderedList/Item.js +139 -0
- package/lib-module/OrderedList/ItemBase.js +28 -0
- package/lib-module/OrderedList/OrderedList.js +71 -0
- package/lib-module/OrderedList/OrderedListBase.js +48 -0
- package/lib-module/OrderedList/constants.js +2 -0
- package/lib-module/OrderedList/index.js +4 -0
- package/lib-module/PreviewCard/AuthorDate.js +53 -0
- package/lib-module/PreviewCard/PreviewCard.js +211 -0
- package/lib-module/PreviewCard/index.js +2 -0
- package/lib-module/PriceLockup/PriceLockup.js +213 -0
- package/lib-module/PriceLockup/index.js +2 -0
- package/lib-module/PriceLockup/tokens.js +120 -0
- package/lib-module/ResponsiveImage/ResponsiveImage.js +100 -0
- package/lib-module/ResponsiveImage/index.js +2 -0
- package/lib-module/Ribbon/Ribbon.js +1 -2
- package/lib-module/Span/Span.js +70 -0
- package/lib-module/Span/index.js +2 -0
- package/lib-module/index.js +10 -0
- package/lib-module/shared/FullBleedContent/FullBleedContent.js +106 -0
- package/lib-module/shared/FullBleedContent/getFullBleedBorderRadius.js +65 -0
- package/lib-module/shared/FullBleedContent/index.js +4 -0
- package/lib-module/shared/FullBleedContent/useFullBleedContentProps.js +65 -0
- package/lib-module/utils/index.js +5 -1
- package/lib-module/utils/logger.js +18 -0
- package/lib-module/utils/media.js +46 -0
- package/lib-module/utils/renderStructuredContent.js +77 -0
- package/lib-module/utils/useTypographyTheme.js +24 -0
- package/package.json +9 -4
- package/src/Breadcrumbs/Breadcrumbs.jsx +222 -0
- package/src/Breadcrumbs/Item/Item.jsx +127 -0
- package/src/Breadcrumbs/index.js +1 -0
- package/src/Callout/Callout.jsx +76 -0
- package/src/Callout/index.js +3 -0
- package/src/DatePicker/CalendarContainer.jsx +210 -0
- package/src/DatePicker/DatePicker.jsx +303 -0
- package/src/DatePicker/dictionary.js +92 -0
- package/src/DatePicker/index.js +3 -0
- package/src/DatePicker/reactDatesCss.js +892 -0
- package/src/ExpandCollapseMini/ExpandCollapseMini.jsx +48 -0
- package/src/ExpandCollapseMini/ExpandCollapseMiniControl.jsx +67 -0
- package/src/ExpandCollapseMini/index.js +3 -0
- package/src/Footnote/Footnote.jsx +468 -0
- package/src/Footnote/FootnoteLink.jsx +120 -0
- package/src/Footnote/dictionary.js +12 -0
- package/src/Footnote/index.js +6 -0
- package/src/OrderedList/Item.jsx +121 -0
- package/src/OrderedList/ItemBase.jsx +18 -0
- package/src/OrderedList/OrderedList.jsx +61 -0
- package/src/OrderedList/OrderedListBase.jsx +38 -0
- package/src/OrderedList/constants.js +2 -0
- package/src/OrderedList/index.js +6 -0
- package/src/PreviewCard/AuthorDate.jsx +31 -0
- package/src/PreviewCard/PreviewCard.jsx +201 -0
- package/src/PreviewCard/index.js +3 -0
- package/src/PriceLockup/PriceLockup.jsx +210 -0
- package/src/PriceLockup/index.js +3 -0
- package/src/PriceLockup/tokens.js +58 -0
- package/src/ResponsiveImage/ResponsiveImage.jsx +77 -0
- package/src/ResponsiveImage/index.js +3 -0
- package/src/Ribbon/Ribbon.jsx +0 -1
- package/src/Span/Span.jsx +66 -0
- package/src/Span/index.js +3 -0
- package/src/index.js +10 -0
- package/src/shared/FullBleedContent/FullBleedContent.jsx +90 -0
- package/src/shared/FullBleedContent/getFullBleedBorderRadius.js +55 -0
- package/src/shared/FullBleedContent/index.js +6 -0
- package/src/shared/FullBleedContent/useFullBleedContentProps.js +63 -0
- package/src/utils/index.js +5 -1
- package/src/utils/logger.js +20 -0
- package/src/utils/media.js +40 -0
- package/src/utils/renderStructuredContent.jsx +73 -0
- package/src/utils/useTypographyTheme.js +14 -0
- package/types/Callout.d.ts +13 -0
- package/types/DatePicker.d.ts +21 -0
- package/types/Footnote.d.ts +21 -0
- package/types/FootnoteLink.d.ts +20 -0
- package/types/PriceLockup.d.ts +22 -0
- 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,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
|