@telus-uds/components-web 1.3.0 → 1.5.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 +32 -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/Paragraph/Paragraph.js +107 -0
- package/lib/Paragraph/index.js +13 -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/Paragraph/Paragraph.js +89 -0
- package/lib-module/Paragraph/index.js +2 -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 -5
- 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/Paragraph/Paragraph.jsx +79 -0
- package/src/Paragraph/index.js +3 -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,79 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import styled from 'styled-components'
|
|
4
|
+
import { selectSystemProps } from '@telus-uds/components-base'
|
|
5
|
+
|
|
6
|
+
import { htmlAttrs, useTypographyTheme } from '../utils'
|
|
7
|
+
|
|
8
|
+
const [selectProps, selectedSystemPropTypes] = selectSystemProps([htmlAttrs])
|
|
9
|
+
|
|
10
|
+
const StyledParagraph = styled.p`
|
|
11
|
+
${({ align }) => (align ? `text-align: ${align};` : '')}
|
|
12
|
+
${({ linesBetween }) => `
|
|
13
|
+
margin-block-start: ${linesBetween}em;
|
|
14
|
+
margin-block-end: ${linesBetween}em;
|
|
15
|
+
`}
|
|
16
|
+
&:first-child {
|
|
17
|
+
margin-block-start: 0em;
|
|
18
|
+
}
|
|
19
|
+
&:last-child {
|
|
20
|
+
margin-block-end: 0em;
|
|
21
|
+
}
|
|
22
|
+
`
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Block text as an HTML ```<p>``` element.
|
|
26
|
+
*
|
|
27
|
+
* ##Usage criteria
|
|
28
|
+
*
|
|
29
|
+
* - All body text should be contained in a **Paragraph**, regardless of length.
|
|
30
|
+
* - If the Paragraph is on a dark background, variant **{ invert: true }** must be used to maintain sufficient colour
|
|
31
|
+
contrast.
|
|
32
|
+
* - All Allium Typography variants other than header size variants are supported.
|
|
33
|
+
*/
|
|
34
|
+
const Paragraph = ({ children, variant, tokens, testID, align, linesBetween = 1, ...rest }) => {
|
|
35
|
+
const style = useTypographyTheme(variant, tokens)
|
|
36
|
+
return (
|
|
37
|
+
<StyledParagraph
|
|
38
|
+
linesBetween={linesBetween}
|
|
39
|
+
data-testid={testID}
|
|
40
|
+
align={align}
|
|
41
|
+
style={style}
|
|
42
|
+
{...selectProps(rest)}
|
|
43
|
+
>
|
|
44
|
+
{children}
|
|
45
|
+
</StyledParagraph>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
Paragraph.propTypes = {
|
|
50
|
+
...selectedSystemPropTypes,
|
|
51
|
+
children: PropTypes.node.isRequired,
|
|
52
|
+
testID: PropTypes.string,
|
|
53
|
+
/**
|
|
54
|
+
* Sets the alignment style for the paragraph. Same options as Typography's `align` prop.
|
|
55
|
+
* 'justify' should be avoided as it usually reduces ease of reading.
|
|
56
|
+
*/
|
|
57
|
+
align: PropTypes.oneOf(['auto', 'left', 'right', 'center', 'justify']),
|
|
58
|
+
/**
|
|
59
|
+
* How much space between consecutive paragraphs, or between a paragraph and its siblings, in CSS
|
|
60
|
+
* `em` units: 1 gives space equal to one line of paragraph text, 0.5 would be half a line, etc.
|
|
61
|
+
* @default 1
|
|
62
|
+
*/
|
|
63
|
+
linesBetween: PropTypes.number,
|
|
64
|
+
/**
|
|
65
|
+
* Paragraph takes the same tokens overrides as Typography
|
|
66
|
+
*/
|
|
67
|
+
tokens: PropTypes.oneOf([PropTypes.object, PropTypes.func]),
|
|
68
|
+
/**
|
|
69
|
+
* Paragraph takes any of Typography's theme variants except for header sizes
|
|
70
|
+
*/
|
|
71
|
+
variant: PropTypes.exact({
|
|
72
|
+
bold: PropTypes.bool,
|
|
73
|
+
colour: PropTypes.oneOf(['secondary', 'tertiary']),
|
|
74
|
+
inverse: PropTypes.bool,
|
|
75
|
+
size: PropTypes.oneOf(['micro', 'small', 'large'])
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default Paragraph
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
|
|
4
|
+
import { StackWrap, Typography, useThemeTokens } from '@telus-uds/components-base'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A footer for PreviewCard that composes author and date in a standard way
|
|
8
|
+
*
|
|
9
|
+
* @TODO
|
|
10
|
+
* Add same locale-based date formatting as StoryCard when ready.
|
|
11
|
+
* Get locale based on decision of https://github.com/telus/universal-design-system/issues/715
|
|
12
|
+
*/
|
|
13
|
+
const AuthorDate = ({ author, date }) => {
|
|
14
|
+
const { separatorColor: color } = useThemeTokens('PreviewCard', {}, {})
|
|
15
|
+
return (
|
|
16
|
+
<StackWrap space={2}>
|
|
17
|
+
<Typography variant={{ size: 'small', colour: 'secondary' }}>{author}</Typography>
|
|
18
|
+
<Typography variant={{ size: 'small' }} tokens={{ color }}>
|
|
19
|
+
·
|
|
20
|
+
</Typography>
|
|
21
|
+
<Typography variant={{ size: 'small', colour: 'secondary' }}>{date}</Typography>
|
|
22
|
+
</StackWrap>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
AuthorDate.propTypes = {
|
|
27
|
+
author: PropTypes.string.isRequired,
|
|
28
|
+
date: PropTypes.string.isRequired
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default AuthorDate
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/* eslint-disable react/require-default-props */
|
|
2
|
+
import React, { forwardRef } from 'react'
|
|
3
|
+
import PropTypes from 'prop-types'
|
|
4
|
+
import omit from 'lodash.omit'
|
|
5
|
+
import {
|
|
6
|
+
Box,
|
|
7
|
+
getThemeTokens,
|
|
8
|
+
hrefAttrsProp,
|
|
9
|
+
PressableCardBase,
|
|
10
|
+
selectSystemProps,
|
|
11
|
+
Spacer,
|
|
12
|
+
StackView,
|
|
13
|
+
Typography,
|
|
14
|
+
a11yProps,
|
|
15
|
+
focusHandlerProps,
|
|
16
|
+
viewProps,
|
|
17
|
+
withLinkRouter,
|
|
18
|
+
useTheme
|
|
19
|
+
} from '@telus-uds/components-base'
|
|
20
|
+
import styled from 'styled-components'
|
|
21
|
+
import FullBleedContent, {
|
|
22
|
+
getFullBleedBorderRadius,
|
|
23
|
+
useFullBleedContentProps
|
|
24
|
+
} from '../shared/FullBleedContent'
|
|
25
|
+
import AuthorDate from './AuthorDate'
|
|
26
|
+
|
|
27
|
+
// Passes React Native-oriented system props through UDS PressableCardBase
|
|
28
|
+
const [selectProps, selectedSystemPropTypes] = selectSystemProps([
|
|
29
|
+
a11yProps,
|
|
30
|
+
focusHandlerProps,
|
|
31
|
+
viewProps
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
// Stop changes to the card's inner border width causing the size and
|
|
35
|
+
// apparent position of the full bleed image to change.
|
|
36
|
+
const FullBleedOffsetOuter = styled.div(({ borderOffset }) => ({
|
|
37
|
+
overflow: 'hidden',
|
|
38
|
+
marginTop: borderOffset * 2
|
|
39
|
+
}))
|
|
40
|
+
const FullBleedOffsetInner = styled.div(({ borderOffset }) => ({
|
|
41
|
+
position: 'relative',
|
|
42
|
+
margin: 0 - borderOffset
|
|
43
|
+
}))
|
|
44
|
+
|
|
45
|
+
const defaultTokens = {
|
|
46
|
+
minWidth: 0,
|
|
47
|
+
shadow: null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Showcase and guide users to another page, with a preview containing title and a footer.
|
|
52
|
+
* Whole card is interactive, width expands based on column or container.
|
|
53
|
+
*
|
|
54
|
+
* - Use `tag` to show category of content
|
|
55
|
+
* - Use `title` to show title of the story
|
|
56
|
+
* - Use `footer` to show text such as the author / date
|
|
57
|
+
* - Use `href` to set the target URL
|
|
58
|
+
* - Use `fullBleedContent` to set the thumbnail image
|
|
59
|
+
*/
|
|
60
|
+
const PreviewCard = forwardRef(
|
|
61
|
+
(
|
|
62
|
+
{
|
|
63
|
+
tag,
|
|
64
|
+
title,
|
|
65
|
+
footer,
|
|
66
|
+
fullBleedImage,
|
|
67
|
+
fullBleedContent = fullBleedImage,
|
|
68
|
+
onPress,
|
|
69
|
+
href,
|
|
70
|
+
...rest
|
|
71
|
+
},
|
|
72
|
+
ref
|
|
73
|
+
) => {
|
|
74
|
+
const { fullBleedContentPosition, contentStackDirection, fullBleedContentProps } =
|
|
75
|
+
useFullBleedContentProps({
|
|
76
|
+
...fullBleedContent,
|
|
77
|
+
position: 'bottom'
|
|
78
|
+
})
|
|
79
|
+
const {
|
|
80
|
+
components: { PreviewCard: theme }
|
|
81
|
+
} = useTheme()
|
|
82
|
+
const getPressableCardTokens = (cardState) => ({
|
|
83
|
+
...omit(getThemeTokens(theme, {}, {}, cardState), 'separatorColor'),
|
|
84
|
+
...defaultTokens
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const { hrefAttrs, rest: unusedRest } = hrefAttrsProp.bundle(rest)
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<PressableCardBase
|
|
91
|
+
onPress={onPress}
|
|
92
|
+
href={href}
|
|
93
|
+
hrefAttrs={hrefAttrs}
|
|
94
|
+
tokens={getPressableCardTokens}
|
|
95
|
+
ref={ref}
|
|
96
|
+
{...selectProps(unusedRest)}
|
|
97
|
+
>
|
|
98
|
+
{(cardState) => {
|
|
99
|
+
const { borderRadius, borderWidth } = getPressableCardTokens(cardState)
|
|
100
|
+
// Stop content jumping around as border size changes
|
|
101
|
+
const borderOffset = borderWidth - theme.tokens.borderWidth
|
|
102
|
+
|
|
103
|
+
const fullBleedBorderRadius = getFullBleedBorderRadius(
|
|
104
|
+
borderRadius + borderOffset,
|
|
105
|
+
fullBleedContentPosition
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<StackView
|
|
110
|
+
direction={contentStackDirection}
|
|
111
|
+
tokens={{ justifyContent: 'space-between', flexGrow: 1 }}
|
|
112
|
+
>
|
|
113
|
+
<Box
|
|
114
|
+
horizontal={{ xs: 4, lg: 5, options: { subtract: borderOffset } }}
|
|
115
|
+
vertical={{ xs: 5, lg: 7, options: { subtract: borderOffset } }}
|
|
116
|
+
>
|
|
117
|
+
{Boolean(tag) && (
|
|
118
|
+
<>
|
|
119
|
+
<Typography variant={{ size: 'eyebrow', colour: 'secondary' }}>
|
|
120
|
+
{tag}
|
|
121
|
+
</Typography>
|
|
122
|
+
<Spacer space={2} />
|
|
123
|
+
</>
|
|
124
|
+
)}
|
|
125
|
+
<Typography variant={{ size: 'h4', colour: 'secondary' }}>{title}</Typography>
|
|
126
|
+
{Boolean(footer) && (
|
|
127
|
+
<>
|
|
128
|
+
<Spacer space={2} />
|
|
129
|
+
{typeof footer === 'string' ? (
|
|
130
|
+
<Typography variant={{ size: 'small', colour: 'secondary' }}>
|
|
131
|
+
{footer}
|
|
132
|
+
</Typography>
|
|
133
|
+
) : (
|
|
134
|
+
footer
|
|
135
|
+
)}
|
|
136
|
+
</>
|
|
137
|
+
)}
|
|
138
|
+
</Box>
|
|
139
|
+
{Boolean(fullBleedContentProps.src || fullBleedContentProps.content) && (
|
|
140
|
+
<FullBleedOffsetOuter borderOffset={borderOffset}>
|
|
141
|
+
<FullBleedOffsetInner borderOffset={borderOffset}>
|
|
142
|
+
<FullBleedContent
|
|
143
|
+
borderRadius={fullBleedBorderRadius}
|
|
144
|
+
{...fullBleedContentProps}
|
|
145
|
+
/>
|
|
146
|
+
</FullBleedOffsetInner>
|
|
147
|
+
</FullBleedOffsetOuter>
|
|
148
|
+
)}
|
|
149
|
+
</StackView>
|
|
150
|
+
)
|
|
151
|
+
}}
|
|
152
|
+
</PressableCardBase>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
PreviewCard.displayName = 'PreviewCard'
|
|
158
|
+
|
|
159
|
+
// Provide standard author/date footer as a preset with the export
|
|
160
|
+
PreviewCard.AuthorDate = AuthorDate
|
|
161
|
+
|
|
162
|
+
PreviewCard.propTypes = {
|
|
163
|
+
...selectedSystemPropTypes,
|
|
164
|
+
/**
|
|
165
|
+
* The URL of the story to be navigated to. Note that `Video` component does not play well when used within
|
|
166
|
+
* a `PreviewCard` with `href` prop set (the volume and progress sliders can be clicked but are not draggable).
|
|
167
|
+
* Please use `onPress` prop instead `href` to organize navigation in cases like this.
|
|
168
|
+
*
|
|
169
|
+
* @see https://github.com/telus/allium-design-system/issues/6
|
|
170
|
+
*/
|
|
171
|
+
href: PropTypes.string,
|
|
172
|
+
/**
|
|
173
|
+
* Optional function to be called on press e.g. for within-page navigation.
|
|
174
|
+
*/
|
|
175
|
+
onPress: PropTypes.func,
|
|
176
|
+
/**
|
|
177
|
+
* Text stating the category of the content.
|
|
178
|
+
*/
|
|
179
|
+
tag: PropTypes.string,
|
|
180
|
+
/**
|
|
181
|
+
* Section containing additional information, such as author and date.
|
|
182
|
+
* Use `<PreviewCard.AuthorDate author={...} date={...} />` here for author and date.
|
|
183
|
+
*/
|
|
184
|
+
footer: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
|
|
185
|
+
/**
|
|
186
|
+
* Text stating the title or headline of the story.
|
|
187
|
+
*/
|
|
188
|
+
title: PropTypes.string.isRequired,
|
|
189
|
+
/**
|
|
190
|
+
* Full bleed image to be placed on the card, deprecated in favor of `fullBleedContent`.
|
|
191
|
+
*
|
|
192
|
+
* @deprecated
|
|
193
|
+
*/
|
|
194
|
+
fullBleedImage: PropTypes.exact(FullBleedContent.propTypes || {}),
|
|
195
|
+
/**
|
|
196
|
+
* Full bleed content to be placed on the card.
|
|
197
|
+
*/
|
|
198
|
+
fullBleedContent: PropTypes.exact(FullBleedContent.propTypes || {})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export default withLinkRouter(PreviewCard)
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import {
|
|
4
|
+
A11yText,
|
|
5
|
+
Divider,
|
|
6
|
+
selectSystemProps,
|
|
7
|
+
Typography,
|
|
8
|
+
useThemeTokens
|
|
9
|
+
} from '@telus-uds/components-base'
|
|
10
|
+
import styled from 'styled-components'
|
|
11
|
+
import FootnoteLink from '../Footnote/FootnoteLink'
|
|
12
|
+
import typographyTokens from './tokens'
|
|
13
|
+
import { htmlAttrs, warn } from '../utils'
|
|
14
|
+
|
|
15
|
+
const [selectProps, selectedSystemPropTypes] = selectSystemProps([htmlAttrs])
|
|
16
|
+
|
|
17
|
+
const PriceLockupContainer = styled.div`
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: column;
|
|
20
|
+
width: fit-content;
|
|
21
|
+
`
|
|
22
|
+
|
|
23
|
+
const PriceContainer = styled.div`
|
|
24
|
+
display: flex;
|
|
25
|
+
margin-bottom: ${({ priceMarginBottom }) => priceMarginBottom};
|
|
26
|
+
`
|
|
27
|
+
|
|
28
|
+
const FootnoteContainer = styled.div`
|
|
29
|
+
display: flex;
|
|
30
|
+
margin-top: ${({ footnoteMarginTop }) => footnoteMarginTop};
|
|
31
|
+
gap: ${({ footnoteGap }) => footnoteGap};
|
|
32
|
+
`
|
|
33
|
+
|
|
34
|
+
const BottomTextContainer = styled.div`
|
|
35
|
+
margin-top: ${({ bottomTextMarginTop }) => bottomTextMarginTop};
|
|
36
|
+
`
|
|
37
|
+
|
|
38
|
+
const BottomLinksContainer = styled.div`
|
|
39
|
+
align-self: center;
|
|
40
|
+
margin-left: ${({ bottomLinksMarginLeft }) => bottomLinksMarginLeft};
|
|
41
|
+
`
|
|
42
|
+
|
|
43
|
+
const TopTextContainer = styled.div`
|
|
44
|
+
margin-bottom: ${({ topTextMarginBottom }) => topTextMarginBottom};
|
|
45
|
+
`
|
|
46
|
+
|
|
47
|
+
const RateTextContainer = styled.div`
|
|
48
|
+
align-self: flex-end;
|
|
49
|
+
`
|
|
50
|
+
|
|
51
|
+
const StrikeThroughContainer = styled.div`
|
|
52
|
+
display: flex;
|
|
53
|
+
position: relative;
|
|
54
|
+
align-items: center;
|
|
55
|
+
::before {
|
|
56
|
+
content: '';
|
|
57
|
+
width: 100%;
|
|
58
|
+
height: ${({ strikeThroughHeight }) => strikeThroughHeight};
|
|
59
|
+
background: ${({ strikeThroughBackground }) => strikeThroughBackground};
|
|
60
|
+
position: absolute;
|
|
61
|
+
}
|
|
62
|
+
`
|
|
63
|
+
|
|
64
|
+
const PriceLockup = ({
|
|
65
|
+
size = 'medium',
|
|
66
|
+
signDirection = 'left',
|
|
67
|
+
footnoteLinks = [],
|
|
68
|
+
topText,
|
|
69
|
+
price,
|
|
70
|
+
currencySymbol = '$',
|
|
71
|
+
rateText,
|
|
72
|
+
bottomText,
|
|
73
|
+
onClickFootnote,
|
|
74
|
+
strikeThrough,
|
|
75
|
+
a11yText,
|
|
76
|
+
tokens: priceLockupTokens,
|
|
77
|
+
variant = {},
|
|
78
|
+
...rest
|
|
79
|
+
}) => {
|
|
80
|
+
const {
|
|
81
|
+
footnoteMarginTop,
|
|
82
|
+
footnoteGap,
|
|
83
|
+
bottomTextMarginTop,
|
|
84
|
+
priceMarginBottom,
|
|
85
|
+
bottomLinksMarginLeft,
|
|
86
|
+
topTextMarginBottom,
|
|
87
|
+
strikeThroughHeight,
|
|
88
|
+
strikeThroughBackground,
|
|
89
|
+
fontColor,
|
|
90
|
+
dividerColor
|
|
91
|
+
} = useThemeTokens('PriceLockup', priceLockupTokens, variant)
|
|
92
|
+
|
|
93
|
+
const priceString = String(price)
|
|
94
|
+
const lastDotPosition = priceString.lastIndexOf('.')
|
|
95
|
+
const lastCommaPosition = priceString.lastIndexOf(',')
|
|
96
|
+
const [separator, separatorPosition] =
|
|
97
|
+
lastDotPosition > lastCommaPosition ? ['.', lastDotPosition] : [',', lastCommaPosition]
|
|
98
|
+
|
|
99
|
+
// If the separator is at the fourth character from the end of the string or more, it's most probably
|
|
100
|
+
// a part of the amount, and the cents are not included in the price string
|
|
101
|
+
const hasCents = separatorPosition !== -1 && separatorPosition >= priceString.length - 3
|
|
102
|
+
const amount = hasCents ? priceString.substring(0, separatorPosition) : priceString
|
|
103
|
+
const cents = hasCents ? priceString.substring(separatorPosition + 1) : null
|
|
104
|
+
|
|
105
|
+
const color = strikeThrough ? strikeThroughBackground : fontColor
|
|
106
|
+
|
|
107
|
+
const renderTypography = (value, tokens) => (
|
|
108
|
+
<Typography tokens={{ ...tokens, color }}>{value}</Typography>
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
const renderCurrencySymbol = () =>
|
|
112
|
+
renderTypography(`${currencySymbol}`, typographyTokens.dollarSign[size])
|
|
113
|
+
|
|
114
|
+
const renderFootnoteLinks = () =>
|
|
115
|
+
footnoteLinks && footnoteLinks.length > 0 ? (
|
|
116
|
+
<FootnoteLink number={footnoteLinks} onClick={onClickFootnote} />
|
|
117
|
+
) : null
|
|
118
|
+
|
|
119
|
+
const renderAmount = () => {
|
|
120
|
+
const amountComponent = renderTypography(amount, typographyTokens.amount[size])
|
|
121
|
+
if (strikeThrough) {
|
|
122
|
+
return (
|
|
123
|
+
<>
|
|
124
|
+
<A11yText text={a11yText} />
|
|
125
|
+
<StrikeThroughContainer
|
|
126
|
+
strikeThroughHeight={`${strikeThroughHeight}px`}
|
|
127
|
+
strikeThroughBackground={strikeThroughBackground}
|
|
128
|
+
>
|
|
129
|
+
{amountComponent}
|
|
130
|
+
</StrikeThroughContainer>
|
|
131
|
+
</>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return amountComponent
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const renderPrice = () => (
|
|
139
|
+
<>
|
|
140
|
+
<PriceContainer priceMarginBottom={`${priceMarginBottom}px`}>
|
|
141
|
+
{signDirection === 'left' && renderCurrencySymbol()}
|
|
142
|
+
{renderAmount()}
|
|
143
|
+
{cents && renderTypography(`${separator}${cents}`, typographyTokens.cents[size])}
|
|
144
|
+
{signDirection === 'right' && <> {renderCurrencySymbol()}</>}
|
|
145
|
+
{rateText && (
|
|
146
|
+
<RateTextContainer>
|
|
147
|
+
{renderTypography(rateText, typographyTokens.rate[size])}
|
|
148
|
+
</RateTextContainer>
|
|
149
|
+
)}
|
|
150
|
+
{!bottomText && footnoteLinks.length <= 3 && (
|
|
151
|
+
<BottomLinksContainer bottomLinksMarginLeft={`${bottomLinksMarginLeft}px`}>
|
|
152
|
+
{renderFootnoteLinks()}
|
|
153
|
+
</BottomLinksContainer>
|
|
154
|
+
)}
|
|
155
|
+
</PriceContainer>
|
|
156
|
+
{!bottomText && footnoteLinks.length > 3 && renderFootnoteLinks()}
|
|
157
|
+
</>
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
const renderFootnoteContent = () => (
|
|
161
|
+
<>
|
|
162
|
+
<FootnoteContainer
|
|
163
|
+
footnoteMarginTop={`${footnoteMarginTop}px`}
|
|
164
|
+
footnoteGap={`${footnoteGap}px`}
|
|
165
|
+
>
|
|
166
|
+
<BottomTextContainer bottomTextMarginTop={`${bottomTextMarginTop}px`}>
|
|
167
|
+
{renderTypography(bottomText, typographyTokens.bottomText[size])}
|
|
168
|
+
</BottomTextContainer>
|
|
169
|
+
{footnoteLinks.length <= 3 && renderFootnoteLinks()}
|
|
170
|
+
</FootnoteContainer>
|
|
171
|
+
{footnoteLinks.length > 3 && renderFootnoteLinks()}
|
|
172
|
+
</>
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if (strikeThrough && !a11yText) {
|
|
176
|
+
warn('PriceLockup', 'a11yText must be provided with strikethrough pricing')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<PriceLockupContainer {...selectProps(rest)}>
|
|
181
|
+
{topText && (
|
|
182
|
+
<TopTextContainer topTextMarginBottom={`${topTextMarginBottom}px`}>
|
|
183
|
+
{renderTypography(topText, typographyTokens.topText[size])}
|
|
184
|
+
</TopTextContainer>
|
|
185
|
+
)}
|
|
186
|
+
{renderPrice()}
|
|
187
|
+
{bottomText && (
|
|
188
|
+
<Divider testID="price-lockup-divider" role="separator" tokens={{ color: dividerColor }} />
|
|
189
|
+
)}
|
|
190
|
+
{bottomText && renderFootnoteContent()}
|
|
191
|
+
</PriceLockupContainer>
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
PriceLockup.propTypes = {
|
|
196
|
+
...selectedSystemPropTypes,
|
|
197
|
+
size: PropTypes.oneOf(['small', 'medium', 'large']),
|
|
198
|
+
currencySymbol: PropTypes.string,
|
|
199
|
+
topText: PropTypes.string,
|
|
200
|
+
price: PropTypes.string.isRequired,
|
|
201
|
+
rateText: PropTypes.string,
|
|
202
|
+
bottomText: PropTypes.string,
|
|
203
|
+
signDirection: PropTypes.oneOf(['right', 'left']),
|
|
204
|
+
footnoteLinks: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])),
|
|
205
|
+
onClickFootnote: PropTypes.func,
|
|
206
|
+
strikeThrough: PropTypes.bool,
|
|
207
|
+
a11yText: PropTypes.string
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export default PriceLockup
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import palette from '@telus-uds/palette-allium/build/rn/palette'
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
fontSize: { size14, size16, size20, size28, size36, size40, size64 },
|
|
5
|
+
lineHeight: {
|
|
6
|
+
ratio1to1,
|
|
7
|
+
ratio10to7,
|
|
8
|
+
ratio3to2,
|
|
9
|
+
ratio8to5,
|
|
10
|
+
ratio5to4,
|
|
11
|
+
ratio6to5,
|
|
12
|
+
ratio9to8,
|
|
13
|
+
ratio4to3,
|
|
14
|
+
ratio8to7
|
|
15
|
+
},
|
|
16
|
+
letterSpacing: { condensed }
|
|
17
|
+
} = palette
|
|
18
|
+
|
|
19
|
+
const typographyTokens = {
|
|
20
|
+
topText: {
|
|
21
|
+
small: { fontSize: size14, lineHeight: ratio10to7 },
|
|
22
|
+
medium: { fontSize: size16, lineHeight: ratio3to2 },
|
|
23
|
+
large: { fontSize: size20, lineHeight: ratio8to5 }
|
|
24
|
+
},
|
|
25
|
+
dollarSign: {
|
|
26
|
+
small: { fontSize: size16, lineHeight: ratio5to4 },
|
|
27
|
+
medium: { fontSize: size20, lineHeight: ratio6to5 },
|
|
28
|
+
large: { fontSize: size36, lineHeight: ratio9to8 }
|
|
29
|
+
},
|
|
30
|
+
// TODO - fontWeight should have its own const on palette
|
|
31
|
+
amount: {
|
|
32
|
+
small: { fontSize: size28, lineHeight: ratio1to1, letterSpacing: condensed, fontWeight: '300' },
|
|
33
|
+
medium: {
|
|
34
|
+
fontSize: size40,
|
|
35
|
+
lineHeight: ratio1to1,
|
|
36
|
+
letterSpacing: condensed,
|
|
37
|
+
fontWeight: '300'
|
|
38
|
+
},
|
|
39
|
+
large: { fontSize: size64, lineHeight: ratio1to1, letterSpacing: condensed, fontWeight: '300' }
|
|
40
|
+
},
|
|
41
|
+
cents: {
|
|
42
|
+
small: { fontSize: size16, lineHeight: ratio4to3 },
|
|
43
|
+
medium: { fontSize: size20, lineHeight: ratio6to5 },
|
|
44
|
+
large: { fontSize: size36, lineHeight: ratio9to8 }
|
|
45
|
+
},
|
|
46
|
+
rate: {
|
|
47
|
+
small: { fontSize: size14, lineHeight: ratio8to7 },
|
|
48
|
+
medium: { fontSize: size16, lineHeight: ratio5to4 },
|
|
49
|
+
large: { fontSize: size20, lineHeight: ratio8to5 }
|
|
50
|
+
},
|
|
51
|
+
bottomText: {
|
|
52
|
+
small: { fontSize: size14, lineHeight: ratio10to7 },
|
|
53
|
+
medium: { fontSize: size16, lineHeight: ratio3to2 },
|
|
54
|
+
large: { fontSize: size20, lineHeight: ratio8to5 }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default typographyTokens
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import { selectSystemProps } from '@telus-uds/components-base'
|
|
4
|
+
import { viewports } from '@telus-uds/system-constants'
|
|
5
|
+
import { htmlAttrs } from '../utils'
|
|
6
|
+
|
|
7
|
+
const [selectProps, selectedSystemPropTypes] = selectSystemProps([htmlAttrs])
|
|
8
|
+
|
|
9
|
+
const staticStyles = {
|
|
10
|
+
image: { display: 'block', width: '100%' }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Provide different image sources for different screen sizes.
|
|
15
|
+
*/
|
|
16
|
+
const ResponsiveImage = ({
|
|
17
|
+
xsSrc,
|
|
18
|
+
smSrc,
|
|
19
|
+
mdSrc,
|
|
20
|
+
lgSrc,
|
|
21
|
+
xlSrc,
|
|
22
|
+
fallbackSrc,
|
|
23
|
+
alt,
|
|
24
|
+
loading = 'eager',
|
|
25
|
+
...rest
|
|
26
|
+
}) => {
|
|
27
|
+
return (
|
|
28
|
+
<picture {...selectProps(rest)}>
|
|
29
|
+
<source srcSet={xlSrc} media={`(min-width: ${viewports.map.get(viewports.xl)}px)`} />
|
|
30
|
+
<source srcSet={lgSrc} media={`(min-width: ${viewports.map.get(viewports.lg)}px)`} />
|
|
31
|
+
<source srcSet={mdSrc} media={`(min-width: ${viewports.map.get(viewports.md)}px)`} />
|
|
32
|
+
<source srcSet={smSrc} media={`(min-width: ${viewports.map.get(viewports.sm)}px)`} />
|
|
33
|
+
<source srcSet={xsSrc} media={`(max-width: ${viewports.map.get(viewports.sm) - 1}px)`} />
|
|
34
|
+
<img src={fallbackSrc} alt={alt} style={staticStyles.image} loading={loading} />
|
|
35
|
+
</picture>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
ResponsiveImage.propTypes = {
|
|
40
|
+
...selectedSystemPropTypes,
|
|
41
|
+
/**
|
|
42
|
+
* The src attribute used for screen widths up to 575px
|
|
43
|
+
*/
|
|
44
|
+
xsSrc: PropTypes.string.isRequired,
|
|
45
|
+
/**
|
|
46
|
+
* The src attribute used for screen widths greater than 576px
|
|
47
|
+
*/
|
|
48
|
+
smSrc: PropTypes.string.isRequired,
|
|
49
|
+
/**
|
|
50
|
+
* The src attribute used for screen widths greater than 768px
|
|
51
|
+
*/
|
|
52
|
+
mdSrc: PropTypes.string,
|
|
53
|
+
/**
|
|
54
|
+
* The src attribute used for screen widths greater than 992px
|
|
55
|
+
*/
|
|
56
|
+
lgSrc: PropTypes.string,
|
|
57
|
+
/**
|
|
58
|
+
* The src attribute used for screen widths greater than 1200px
|
|
59
|
+
*/
|
|
60
|
+
xlSrc: PropTypes.string,
|
|
61
|
+
/**
|
|
62
|
+
* The src attribute used for browsers that don't support responsive images (InternetExplorer)
|
|
63
|
+
*/
|
|
64
|
+
fallbackSrc: PropTypes.string.isRequired,
|
|
65
|
+
/**
|
|
66
|
+
* The alt attribute for the HTML img element. Setting this attribute to an empty string (alt="") indicates that this image is not a key part of the content, and that non-visual browsers may omit it from rendering.
|
|
67
|
+
*/
|
|
68
|
+
alt: PropTypes.string.isRequired,
|
|
69
|
+
/**
|
|
70
|
+
* Loading strategy.
|
|
71
|
+
* @default 'eager'
|
|
72
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-loading
|
|
73
|
+
*/
|
|
74
|
+
loading: PropTypes.oneOf(['eager', 'lazy'])
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export default ResponsiveImage
|