@telus-uds/components-web 1.9.0 → 1.11.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 +40 -2
- package/lib/Breadcrumbs/Breadcrumbs.js +8 -3
- package/lib/Breadcrumbs/Item/Item.js +31 -6
- package/lib/Callout/Callout.js +24 -3
- package/lib/Disclaimer/Disclaimer.js +72 -0
- package/lib/Disclaimer/index.js +15 -0
- package/lib/Footnote/Footnote.js +70 -28
- package/lib/Footnote/FootnoteLink.js +11 -13
- package/lib/NavigationBar/NavigationBar.js +231 -0
- package/lib/NavigationBar/NavigationItem.js +111 -0
- package/lib/NavigationBar/NavigationSubMenu.js +179 -0
- package/lib/NavigationBar/collapseItems.js +51 -0
- package/lib/NavigationBar/index.js +13 -0
- package/lib/PriceLockup/PriceLockup.js +40 -17
- package/lib/PriceLockup/tokens.js +49 -116
- package/lib/Progress/ProgressBar.js +100 -0
- package/lib/Progress/index.js +16 -0
- package/lib/Ribbon/Ribbon.js +53 -32
- package/lib/Spinner/Spinner.js +18 -14
- package/lib/Table/Cell.js +15 -1
- package/lib/Toast/Toast.js +15 -8
- package/lib/VideoPicker/VideoPicker.js +177 -0
- package/lib/VideoPicker/VideoPickerPlayer.js +54 -0
- package/lib/VideoPicker/VideoPickerThumbnail.js +201 -0
- package/lib/VideoPicker/VideoSlider.js +100 -0
- package/lib/VideoPicker/index.js +13 -0
- package/lib/VideoPicker/videoPropType.js +25 -0
- package/lib/index.js +37 -1
- package/lib-module/Breadcrumbs/Breadcrumbs.js +8 -3
- package/lib-module/Breadcrumbs/Item/Item.js +32 -7
- package/lib-module/Callout/Callout.js +24 -3
- package/lib-module/Disclaimer/Disclaimer.js +54 -0
- package/lib-module/Disclaimer/index.js +1 -0
- package/lib-module/Footnote/Footnote.js +68 -27
- package/lib-module/Footnote/FootnoteLink.js +12 -14
- package/lib-module/NavigationBar/NavigationBar.js +207 -0
- package/lib-module/NavigationBar/NavigationItem.js +87 -0
- package/lib-module/NavigationBar/NavigationSubMenu.js +161 -0
- package/lib-module/NavigationBar/collapseItems.js +43 -0
- package/lib-module/NavigationBar/index.js +2 -0
- package/lib-module/PriceLockup/PriceLockup.js +42 -19
- package/lib-module/PriceLockup/tokens.js +54 -119
- package/lib-module/Progress/ProgressBar.js +83 -0
- package/lib-module/Progress/index.js +4 -0
- package/lib-module/Ribbon/Ribbon.js +53 -32
- package/lib-module/Spinner/Spinner.js +17 -14
- package/lib-module/Table/Cell.js +15 -1
- package/lib-module/Toast/Toast.js +15 -8
- package/lib-module/VideoPicker/VideoPicker.js +151 -0
- package/lib-module/VideoPicker/VideoPickerPlayer.js +41 -0
- package/lib-module/VideoPicker/VideoPickerThumbnail.js +180 -0
- package/lib-module/VideoPicker/VideoSlider.js +83 -0
- package/lib-module/VideoPicker/index.js +2 -0
- package/lib-module/VideoPicker/videoPropType.js +9 -0
- package/lib-module/index.js +4 -0
- package/package.json +3 -3
- package/src/Breadcrumbs/Breadcrumbs.jsx +4 -3
- package/src/Breadcrumbs/Item/Item.jsx +18 -4
- package/src/Callout/Callout.jsx +27 -3
- package/src/Disclaimer/Disclaimer.jsx +39 -0
- package/src/Disclaimer/index.js +1 -0
- package/src/Footnote/Footnote.jsx +76 -26
- package/src/Footnote/FootnoteLink.jsx +28 -18
- package/src/NavigationBar/NavigationBar.jsx +217 -0
- package/src/NavigationBar/NavigationItem.jsx +83 -0
- package/src/NavigationBar/NavigationSubMenu.jsx +121 -0
- package/src/NavigationBar/collapseItems.js +29 -0
- package/src/NavigationBar/index.js +3 -0
- package/src/PriceLockup/PriceLockup.jsx +47 -21
- package/src/PriceLockup/tokens.js +34 -54
- package/src/Progress/ProgressBar.jsx +67 -0
- package/src/Progress/index.js +6 -0
- package/src/Ribbon/Ribbon.jsx +21 -9
- package/src/Spinner/Spinner.jsx +20 -17
- package/src/Table/Cell.jsx +22 -5
- package/src/Toast/Toast.jsx +12 -5
- package/src/VideoPicker/VideoPicker.jsx +144 -0
- package/src/VideoPicker/VideoPickerPlayer.jsx +21 -0
- package/src/VideoPicker/VideoPickerThumbnail.jsx +182 -0
- package/src/VideoPicker/VideoSlider.jsx +85 -0
- package/src/VideoPicker/index.js +3 -0
- package/src/VideoPicker/videoPropType.js +12 -0
- package/src/index.js +4 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import styled from 'styled-components'
|
|
4
|
+
import { applyTextStyles, selectSystemProps, useThemeTokens } from '@telus-uds/components-base'
|
|
5
|
+
import { htmlAttrs } from '../utils'
|
|
6
|
+
|
|
7
|
+
const [selectProps, selectedSystemPropTypes] = selectSystemProps([htmlAttrs])
|
|
8
|
+
|
|
9
|
+
const StyledDisclaimer = styled.p(({ fontName, fontWeight, fontSize, ...tokens }) => {
|
|
10
|
+
const { fontFamily } = applyTextStyles({ fontName, fontWeight })
|
|
11
|
+
return {
|
|
12
|
+
fontSize: `${fontSize}px`,
|
|
13
|
+
fontFamily,
|
|
14
|
+
...tokens
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Use Disclaimer to display singular legal statement and must be displayed
|
|
20
|
+
* immediately adjacent to the related, originating content.
|
|
21
|
+
*/
|
|
22
|
+
const Disclaimer = ({ children, ...rest }) => {
|
|
23
|
+
const themeTokens = useThemeTokens('Disclaimer')
|
|
24
|
+
return (
|
|
25
|
+
<StyledDisclaimer {...selectProps(rest)} {...themeTokens}>
|
|
26
|
+
{children}
|
|
27
|
+
</StyledDisclaimer>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
Disclaimer.propTypes = {
|
|
32
|
+
...selectedSystemPropTypes,
|
|
33
|
+
/**
|
|
34
|
+
* The content
|
|
35
|
+
*/
|
|
36
|
+
children: PropTypes.node.isRequired
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default Disclaimer
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './Disclaimer'
|
|
@@ -12,10 +12,9 @@ import {
|
|
|
12
12
|
useThemeTokens
|
|
13
13
|
} from '@telus-uds/components-base'
|
|
14
14
|
|
|
15
|
-
import Close from '../../__fixtures__/icons/Close'
|
|
16
15
|
import OrderedListBase from '../OrderedList/OrderedListBase'
|
|
17
16
|
import { htmlAttrs, media, renderStructuredContent } from '../utils'
|
|
18
|
-
import
|
|
17
|
+
import defaultDictionary from './dictionary'
|
|
19
18
|
|
|
20
19
|
const [selectProps, selectedSystemPropTypes] = selectSystemProps([htmlAttrs])
|
|
21
20
|
|
|
@@ -154,6 +153,10 @@ const ContentContainer = styled.div(
|
|
|
154
153
|
})
|
|
155
154
|
)
|
|
156
155
|
|
|
156
|
+
const StyledCustomContentContainer = styled.div(({ color }) => ({
|
|
157
|
+
color,
|
|
158
|
+
fontFamily: 'HelveticaNow400normal'
|
|
159
|
+
}))
|
|
157
160
|
const usePrevious = (value) => {
|
|
158
161
|
const ref = useRef()
|
|
159
162
|
useEffect(() => {
|
|
@@ -184,7 +187,17 @@ const usePrevious = (value) => {
|
|
|
184
187
|
* - When `Footnote` is closed, focus must return to the initiating element
|
|
185
188
|
*/
|
|
186
189
|
const Footnote = (props) => {
|
|
187
|
-
const {
|
|
190
|
+
const {
|
|
191
|
+
copy,
|
|
192
|
+
number,
|
|
193
|
+
content,
|
|
194
|
+
onClose,
|
|
195
|
+
isOpen,
|
|
196
|
+
tokens,
|
|
197
|
+
variant = {},
|
|
198
|
+
dictionary = defaultDictionary,
|
|
199
|
+
...rest
|
|
200
|
+
} = props
|
|
188
201
|
const {
|
|
189
202
|
footnoteBackground,
|
|
190
203
|
footnoteBorderTopSizeMd,
|
|
@@ -210,13 +223,14 @@ const Footnote = (props) => {
|
|
|
210
223
|
closeButtonMarginRight,
|
|
211
224
|
closeButtonMarginBottom,
|
|
212
225
|
closeButtonWidth,
|
|
213
|
-
closeButtonIconSize
|
|
226
|
+
closeButtonIconSize,
|
|
227
|
+
closeIcon
|
|
214
228
|
} = useThemeTokens('Footnote', tokens, variant)
|
|
215
229
|
|
|
216
230
|
const footnoteRef = useRef(null)
|
|
217
231
|
const headerRef = useRef(null)
|
|
218
232
|
const bodyRef = useRef(null)
|
|
219
|
-
const
|
|
233
|
+
const contentRef = useRef(null)
|
|
220
234
|
const headingRef = useRef(null)
|
|
221
235
|
const [data, setData] = useState({ content: null, number: null })
|
|
222
236
|
const [headerHeight, setHeaderHeight] = useState('auto')
|
|
@@ -265,7 +279,7 @@ const Footnote = (props) => {
|
|
|
265
279
|
)
|
|
266
280
|
|
|
267
281
|
const saveCurrentHeight = () => {
|
|
268
|
-
const oldHeight =
|
|
282
|
+
const oldHeight = contentRef.current.offsetHeight
|
|
269
283
|
setBodyHeight(oldHeight)
|
|
270
284
|
}
|
|
271
285
|
|
|
@@ -287,14 +301,14 @@ const Footnote = (props) => {
|
|
|
287
301
|
event.persist()
|
|
288
302
|
if (event.propertyName === 'opacity' && !isTextVisible) {
|
|
289
303
|
setData({ content, number })
|
|
290
|
-
if (bodyHeight !==
|
|
304
|
+
if (bodyHeight !== contentRef.current.offsetHeight) {
|
|
291
305
|
// Set new height
|
|
292
|
-
setBodyHeight(
|
|
306
|
+
setBodyHeight(contentRef.current.offsetHeight)
|
|
293
307
|
} else {
|
|
294
308
|
setIsTextVisible(true)
|
|
295
309
|
}
|
|
296
310
|
} else {
|
|
297
|
-
setBodyHeight(
|
|
311
|
+
setBodyHeight(contentRef.current.offsetHeight)
|
|
298
312
|
}
|
|
299
313
|
|
|
300
314
|
if (event.propertyName === 'height' && !isTextVisible) {
|
|
@@ -359,6 +373,41 @@ const Footnote = (props) => {
|
|
|
359
373
|
// Reset footnote on close
|
|
360
374
|
useEffect(resetFootnote, [isOpen])
|
|
361
375
|
|
|
376
|
+
const getFootnoteBodyContent = useCallback(() => {
|
|
377
|
+
if (!data.number || !data.content) {
|
|
378
|
+
return null
|
|
379
|
+
}
|
|
380
|
+
if (React.isValidElement(data.content)) {
|
|
381
|
+
return (
|
|
382
|
+
<StyledCustomContentContainer ref={contentRef}>{data.content}</StyledCustomContentContainer>
|
|
383
|
+
)
|
|
384
|
+
}
|
|
385
|
+
return (
|
|
386
|
+
<List start={data.number} ref={contentRef} listPaddingLeft={listPaddingLeft}>
|
|
387
|
+
<ListItem
|
|
388
|
+
listItemMarkerFontSize={listItemMarkerFontSize}
|
|
389
|
+
listItemMarkerLineHeight={listItemMarkerLineHeight}
|
|
390
|
+
listItemColor={listItemColor}
|
|
391
|
+
listItemFontSize={listItemFontSize}
|
|
392
|
+
listItemLineHeight={listItemLineHeight}
|
|
393
|
+
listItemPaddingLeft={listItemPaddingLeft}
|
|
394
|
+
>
|
|
395
|
+
<Typography>{renderStructuredContent(data.content)}</Typography>
|
|
396
|
+
</ListItem>
|
|
397
|
+
</List>
|
|
398
|
+
)
|
|
399
|
+
}, [
|
|
400
|
+
data.content,
|
|
401
|
+
data.number,
|
|
402
|
+
listItemColor,
|
|
403
|
+
listItemFontSize,
|
|
404
|
+
listItemLineHeight,
|
|
405
|
+
listItemMarkerFontSize,
|
|
406
|
+
listItemMarkerLineHeight,
|
|
407
|
+
listItemPaddingLeft,
|
|
408
|
+
listPaddingLeft
|
|
409
|
+
])
|
|
410
|
+
|
|
362
411
|
return (
|
|
363
412
|
<Portal>
|
|
364
413
|
<div {...selectProps(rest)}>
|
|
@@ -388,7 +437,7 @@ const Footnote = (props) => {
|
|
|
388
437
|
}}
|
|
389
438
|
aria-label={getCopy('close')}
|
|
390
439
|
>
|
|
391
|
-
<Icon icon={
|
|
440
|
+
<Icon icon={closeIcon} tokens={{ size: `${closeButtonIconSize}px` }} />
|
|
392
441
|
</CloseButton>
|
|
393
442
|
</StyledHeader>
|
|
394
443
|
</StyledFootnoteHeader>
|
|
@@ -402,20 +451,7 @@ const Footnote = (props) => {
|
|
|
402
451
|
footnoteBodyBackground={footnoteBodyBackground}
|
|
403
452
|
footnoteBodyPadding={`${footnoteBodyPaddingTop}px ${footnoteBodyPaddingRight}px ${footnoteBodyPaddingBottom}px ${footnoteBodyPaddingLeft}px`}
|
|
404
453
|
>
|
|
405
|
-
{
|
|
406
|
-
<List start={data.number} ref={listRef} listPaddingLeft={listPaddingLeft}>
|
|
407
|
-
<ListItem
|
|
408
|
-
listItemMarkerFontSize={listItemMarkerFontSize}
|
|
409
|
-
listItemMarkerLineHeight={listItemMarkerLineHeight}
|
|
410
|
-
listItemColor={listItemColor}
|
|
411
|
-
listItemFontSize={listItemFontSize}
|
|
412
|
-
listItemLineHeight={listItemLineHeight}
|
|
413
|
-
listItemPaddingLeft={listItemPaddingLeft}
|
|
414
|
-
>
|
|
415
|
-
<Typography>{renderStructuredContent(data.content)}</Typography>
|
|
416
|
-
</ListItem>
|
|
417
|
-
</List>
|
|
418
|
-
)}
|
|
454
|
+
{getFootnoteBodyContent()}
|
|
419
455
|
</StyledFootnoteBody>
|
|
420
456
|
</ContentContainer>
|
|
421
457
|
</StyledFootnote>
|
|
@@ -429,12 +465,19 @@ const copyShape = PropTypes.shape({
|
|
|
429
465
|
heading: PropTypes.string.isRequired
|
|
430
466
|
})
|
|
431
467
|
|
|
468
|
+
// If a language dictionary entry is provided, it must contain every key
|
|
469
|
+
const dictionaryContentShape = PropTypes.shape({
|
|
470
|
+
a11yLabel: PropTypes.string.isRequired,
|
|
471
|
+
close: PropTypes.string.isRequired,
|
|
472
|
+
heading: PropTypes.string.isRequired
|
|
473
|
+
})
|
|
474
|
+
|
|
432
475
|
Footnote.propTypes = {
|
|
433
476
|
...selectedSystemPropTypes,
|
|
434
477
|
/**
|
|
435
478
|
* The content.
|
|
436
479
|
*/
|
|
437
|
-
content: PropTypes.string,
|
|
480
|
+
content: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
|
|
438
481
|
/**
|
|
439
482
|
* Use the `copy` prop to either select provided English or French copy by passing 'en' or 'fr' respectively.
|
|
440
483
|
* To provide your own, pass a JSON object with the keys `heading` and `close`.
|
|
@@ -455,7 +498,14 @@ Footnote.propTypes = {
|
|
|
455
498
|
* @param {Object} options Custom options
|
|
456
499
|
* @param {boolean} options.returnFocus Should the `Footnote` return focus on close
|
|
457
500
|
*/
|
|
458
|
-
onClose: PropTypes.func.isRequired
|
|
501
|
+
onClose: PropTypes.func.isRequired,
|
|
502
|
+
/**
|
|
503
|
+
* Override the default dictionary, by passing the complete dictionary object for `en` and `fr`
|
|
504
|
+
*/
|
|
505
|
+
dictionary: PropTypes.shape({
|
|
506
|
+
en: dictionaryContentShape,
|
|
507
|
+
fr: dictionaryContentShape
|
|
508
|
+
})
|
|
459
509
|
}
|
|
460
510
|
|
|
461
511
|
Footnote.defaultProps = {
|
|
@@ -1,24 +1,36 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import PropTypes from 'prop-types'
|
|
3
3
|
import styled from 'styled-components'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
applyTextStyles,
|
|
6
|
+
selectSystemProps,
|
|
7
|
+
useCopy,
|
|
8
|
+
useThemeTokens
|
|
9
|
+
} from '@telus-uds/components-base'
|
|
5
10
|
import dictionary from './dictionary'
|
|
6
11
|
import { htmlAttrs } from '../utils'
|
|
7
12
|
|
|
8
13
|
const [selectProps, selectedSystemPropTypes] = selectSystemProps([htmlAttrs])
|
|
9
14
|
|
|
10
|
-
const StyledSup = styled.sup(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
const StyledSup = styled.sup(
|
|
16
|
+
({ fontSize, lineHeight, paddingLeft, paddingRight, ...fontNameAndWeight }) => {
|
|
17
|
+
return {
|
|
18
|
+
border: 0,
|
|
19
|
+
color: 'inherit',
|
|
20
|
+
cursor: 'pointer',
|
|
21
|
+
// we want to fallback on 'smaller' but have a valid size when a custom font size is provided.
|
|
22
|
+
fontSize: fontSize ? `${fontSize}px` : 'smaller',
|
|
23
|
+
lineHeight,
|
|
24
|
+
margin: 0,
|
|
25
|
+
paddingVertical: 0,
|
|
26
|
+
paddingLeft,
|
|
27
|
+
paddingRight,
|
|
28
|
+
textDecoration: 'underline',
|
|
29
|
+
// apply font family
|
|
30
|
+
...applyTextStyles(fontNameAndWeight)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
)
|
|
22
34
|
|
|
23
35
|
/**
|
|
24
36
|
* Use `FootnoteLink` to open `Footnote` component and display related legal content.
|
|
@@ -38,7 +50,7 @@ const FootnoteLink = ({
|
|
|
38
50
|
variant = {},
|
|
39
51
|
...rest
|
|
40
52
|
}) => {
|
|
41
|
-
const
|
|
53
|
+
const themeTokens = useThemeTokens('FootnoteLink', tokens, variant)
|
|
42
54
|
const numbers = Array.isArray(number) ? number : [number]
|
|
43
55
|
const refs = numbers.map(() => React.createRef())
|
|
44
56
|
const handleClick = (index) => {
|
|
@@ -68,11 +80,9 @@ const FootnoteLink = ({
|
|
|
68
80
|
key={num}
|
|
69
81
|
ref={refs[index]}
|
|
70
82
|
onClick={(event) => handleOnClick(event, index)}
|
|
71
|
-
fontSize={fontSize}
|
|
72
|
-
lineHeight={lineHeight}
|
|
73
|
-
paddingLeft={paddingLeft}
|
|
74
|
-
paddingRight={paddingRight}
|
|
75
83
|
{...selectProps(rest)}
|
|
84
|
+
{...themeTokens}
|
|
85
|
+
fontSize={fontSize}
|
|
76
86
|
>
|
|
77
87
|
{`${num}${index !== numbers.length - 1 ? ',' : ''}`}
|
|
78
88
|
</StyledSup>
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import React, { forwardRef, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import {
|
|
4
|
+
selectSystemProps,
|
|
5
|
+
StackView,
|
|
6
|
+
Typography,
|
|
7
|
+
useResponsiveProp,
|
|
8
|
+
withLinkRouter
|
|
9
|
+
} from '@telus-uds/components-base'
|
|
10
|
+
import styled from 'styled-components'
|
|
11
|
+
import { htmlAttrs } from '../utils'
|
|
12
|
+
import NavigationItem from './NavigationItem'
|
|
13
|
+
import NavigationSubMenu from './NavigationSubMenu'
|
|
14
|
+
import collapseItems from './collapseItems'
|
|
15
|
+
|
|
16
|
+
const [selectProps, selectedSystemPropTypes] = selectSystemProps([htmlAttrs])
|
|
17
|
+
|
|
18
|
+
const Heading = styled.div({
|
|
19
|
+
alignItems: 'center',
|
|
20
|
+
display: 'flex',
|
|
21
|
+
flex: 1,
|
|
22
|
+
justifyContent: 'flex-start',
|
|
23
|
+
'> *': { display: 'contents', letterSpacing: 0 }
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* NavigationBar can be used to allow customers to consistently navigate across
|
|
28
|
+
* key pages within a specific product line
|
|
29
|
+
*/
|
|
30
|
+
const NavigationBar = forwardRef(
|
|
31
|
+
(
|
|
32
|
+
{
|
|
33
|
+
accessibilityRole = 'navigation',
|
|
34
|
+
heading,
|
|
35
|
+
headingLevel = 'h1',
|
|
36
|
+
items,
|
|
37
|
+
onChange = () => {},
|
|
38
|
+
selectedId,
|
|
39
|
+
LinkRouter,
|
|
40
|
+
linkRouterProps,
|
|
41
|
+
...rest
|
|
42
|
+
},
|
|
43
|
+
ref
|
|
44
|
+
) => {
|
|
45
|
+
const direction = useResponsiveProp({ xs: 'column', sm: 'row' })
|
|
46
|
+
const itemsForViewport = useResponsiveProp({ xs: collapseItems(items, selectedId), lg: items })
|
|
47
|
+
const openOverlayRef = useRef(null)
|
|
48
|
+
const [openSubMenuId, setOpenSubMenuId] = useState(null)
|
|
49
|
+
const handleSubMenuClose = (event) => {
|
|
50
|
+
if (event.type === 'keydown') {
|
|
51
|
+
if (event.key === 'Escape' || event.key === 27) {
|
|
52
|
+
setOpenSubMenuId(null)
|
|
53
|
+
}
|
|
54
|
+
} else if (
|
|
55
|
+
event.type === 'click' &&
|
|
56
|
+
openOverlayRef?.current &&
|
|
57
|
+
event.target &&
|
|
58
|
+
!openOverlayRef?.current?.contains(event.target)
|
|
59
|
+
) {
|
|
60
|
+
setOpenSubMenuId(null)
|
|
61
|
+
} else if (
|
|
62
|
+
event.type === 'touchstart' &&
|
|
63
|
+
openOverlayRef?.current &&
|
|
64
|
+
event.touches[0].target &&
|
|
65
|
+
!openOverlayRef?.current?.contains(event.touches[0].target)
|
|
66
|
+
) {
|
|
67
|
+
setOpenSubMenuId(null)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Add listeners for mouse clicks outside and for ESCAPE key presses
|
|
72
|
+
// TODO: create a custom hook for that and use here and in the Footnote
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (openSubMenuId !== null) {
|
|
75
|
+
window.addEventListener('click', handleSubMenuClose)
|
|
76
|
+
window.addEventListener('keydown', handleSubMenuClose)
|
|
77
|
+
window.addEventListener('touchstart', handleSubMenuClose)
|
|
78
|
+
}
|
|
79
|
+
return () => {
|
|
80
|
+
if (openSubMenuId !== null) {
|
|
81
|
+
window.removeEventListener('click', handleSubMenuClose)
|
|
82
|
+
window.removeEventListener('keydown', handleSubMenuClose)
|
|
83
|
+
window.removeEventListener('touchstart', handleSubMenuClose)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}, [openSubMenuId])
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<StackView
|
|
90
|
+
accessibilityRole={accessibilityRole}
|
|
91
|
+
direction={direction}
|
|
92
|
+
ref={ref}
|
|
93
|
+
space={2}
|
|
94
|
+
tokens={{
|
|
95
|
+
alignItems: direction === 'column' ? 'flex-start' : 'center',
|
|
96
|
+
justifyContent: 'flex-end'
|
|
97
|
+
}}
|
|
98
|
+
{...selectProps(rest)}
|
|
99
|
+
>
|
|
100
|
+
{heading && (
|
|
101
|
+
<Heading>
|
|
102
|
+
<Typography variant={{ size: 'h5' }} heading={headingLevel}>
|
|
103
|
+
{heading}
|
|
104
|
+
</Typography>
|
|
105
|
+
</Heading>
|
|
106
|
+
)}
|
|
107
|
+
{itemsForViewport?.map(
|
|
108
|
+
(
|
|
109
|
+
{
|
|
110
|
+
href,
|
|
111
|
+
label,
|
|
112
|
+
id,
|
|
113
|
+
onClick,
|
|
114
|
+
ref: itemRef,
|
|
115
|
+
LinkRouter: ItemLinkRouter = LinkRouter,
|
|
116
|
+
linkRouterProps: itemLinkRouterProps,
|
|
117
|
+
items: nestedItems,
|
|
118
|
+
...itemRest
|
|
119
|
+
},
|
|
120
|
+
index
|
|
121
|
+
) => {
|
|
122
|
+
const itemId = id ?? label
|
|
123
|
+
const handleClick = (event) => {
|
|
124
|
+
if (nestedItems) {
|
|
125
|
+
setOpenSubMenuId(openSubMenuId !== itemId ? itemId : null)
|
|
126
|
+
}
|
|
127
|
+
onClick?.(event)
|
|
128
|
+
onChange?.(itemId, event)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const ItemComponent = nestedItems ? NavigationSubMenu : NavigationItem
|
|
132
|
+
const isOpen = itemId === openSubMenuId
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<ItemComponent
|
|
136
|
+
ref={itemRef}
|
|
137
|
+
key={itemId}
|
|
138
|
+
href={href}
|
|
139
|
+
onClick={handleClick}
|
|
140
|
+
// TODO: refactor to pass selected ID via context
|
|
141
|
+
selectedId={selectedId}
|
|
142
|
+
index={index}
|
|
143
|
+
LinkRouter={ItemLinkRouter}
|
|
144
|
+
linkRouterProps={{ ...linkRouterProps, ...itemLinkRouterProps }}
|
|
145
|
+
items={nestedItems}
|
|
146
|
+
selected={itemId === selectedId}
|
|
147
|
+
{...itemRest}
|
|
148
|
+
{...(nestedItems && { isOpen })}
|
|
149
|
+
{...(nestedItems && isOpen && { openOverlayRef })}
|
|
150
|
+
>
|
|
151
|
+
{label}
|
|
152
|
+
</ItemComponent>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
)}
|
|
156
|
+
</StackView>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
NavigationBar.displayName = 'NavigationBar'
|
|
162
|
+
|
|
163
|
+
NavigationBar.propTypes = {
|
|
164
|
+
...selectedSystemPropTypes,
|
|
165
|
+
...withLinkRouter.propTypes,
|
|
166
|
+
/**
|
|
167
|
+
* NavigationBar pages
|
|
168
|
+
*
|
|
169
|
+
* Each `item` object must contain:
|
|
170
|
+
* - `heading` - user-facing text in the tab link
|
|
171
|
+
* - `href` - the URL of the page linked to. Do not use hash links, for content within a page, use `Tabs`.
|
|
172
|
+
* - `id` - a stable, unique identifier of the page within the set. Not written into the HTML.
|
|
173
|
+
*/
|
|
174
|
+
items: PropTypes.arrayOf(
|
|
175
|
+
PropTypes.shape({
|
|
176
|
+
label: PropTypes.string.isRequired,
|
|
177
|
+
href: PropTypes.string,
|
|
178
|
+
id: PropTypes.string.isRequired,
|
|
179
|
+
onClick: PropTypes.func,
|
|
180
|
+
selected: PropTypes.bool,
|
|
181
|
+
LinkRouter: withLinkRouter.propTypes?.LinkRouter,
|
|
182
|
+
linkRouterProps: withLinkRouter.propTypes?.linkRouterProps,
|
|
183
|
+
// One layer of nested links is allowed
|
|
184
|
+
items: PropTypes.arrayOf(
|
|
185
|
+
PropTypes.shape({
|
|
186
|
+
label: PropTypes.string.isRequired,
|
|
187
|
+
href: PropTypes.string,
|
|
188
|
+
id: PropTypes.string.isRequired,
|
|
189
|
+
onClick: PropTypes.func,
|
|
190
|
+
selected: PropTypes.bool,
|
|
191
|
+
LinkRouter: withLinkRouter.propTypes?.LinkRouter,
|
|
192
|
+
linkRouterProps: withLinkRouter.propTypes?.linkRouterProps
|
|
193
|
+
})
|
|
194
|
+
)
|
|
195
|
+
})
|
|
196
|
+
).isRequired,
|
|
197
|
+
/**
|
|
198
|
+
* Common navigation bar heading.
|
|
199
|
+
*/
|
|
200
|
+
heading: PropTypes.string,
|
|
201
|
+
headingLevel: PropTypes.oneOf(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']),
|
|
202
|
+
/**
|
|
203
|
+
* Matches the `id` property of the item in `items` corresponding to the current page
|
|
204
|
+
*/
|
|
205
|
+
selectedId: PropTypes.string.isRequired,
|
|
206
|
+
/**
|
|
207
|
+
* Optional function to be called on pressing a link
|
|
208
|
+
*/
|
|
209
|
+
onChange: PropTypes.func
|
|
210
|
+
}
|
|
211
|
+
NavigationBar.defaultProps = {
|
|
212
|
+
heading: undefined,
|
|
213
|
+
headingLevel: 'h1',
|
|
214
|
+
onChange: () => {}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export default NavigationBar
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
selectSystemProps,
|
|
6
|
+
useResponsiveProp,
|
|
7
|
+
useViewport,
|
|
8
|
+
withLinkRouter,
|
|
9
|
+
useThemeTokensCallback
|
|
10
|
+
} from '@telus-uds/components-base'
|
|
11
|
+
import styled from 'styled-components'
|
|
12
|
+
import { htmlAttrs } from '../utils'
|
|
13
|
+
|
|
14
|
+
const [selectProps, selectedSystemPropTypes] = selectSystemProps([htmlAttrs])
|
|
15
|
+
|
|
16
|
+
const defaultMaxWidth = 192
|
|
17
|
+
|
|
18
|
+
const ItemContainer = styled.div(({ targetWidth }) => ({
|
|
19
|
+
display: 'flex',
|
|
20
|
+
flexDirection: 'column',
|
|
21
|
+
justifyContent: 'center',
|
|
22
|
+
maxWidth: `${Math.max(defaultMaxWidth, targetWidth ?? 0)}px`,
|
|
23
|
+
flexGrow: targetWidth ? 1 : 0,
|
|
24
|
+
flexShrink: 1
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* NavigationItem within a NavigationBar component.
|
|
29
|
+
*
|
|
30
|
+
* This is rendered automatically by `NavigationBar` and isn't intended be used directly.
|
|
31
|
+
*/
|
|
32
|
+
const NavigationItem = forwardRef(
|
|
33
|
+
(
|
|
34
|
+
{
|
|
35
|
+
accessibilityRole = 'link', // @todo switch to 'button' for dropdowns
|
|
36
|
+
children,
|
|
37
|
+
id,
|
|
38
|
+
onClick: handleClick = () => {},
|
|
39
|
+
selected,
|
|
40
|
+
accessibilityState = { current: selected ? 'page' : false },
|
|
41
|
+
href,
|
|
42
|
+
tokens,
|
|
43
|
+
variant = {},
|
|
44
|
+
...rest
|
|
45
|
+
},
|
|
46
|
+
ref
|
|
47
|
+
) => {
|
|
48
|
+
const selectedProps = selectProps(rest)
|
|
49
|
+
const targetWidth = useResponsiveProp({ xs: 288, lg: null })
|
|
50
|
+
const viewport = useViewport()
|
|
51
|
+
const getTokens = useThemeTokensCallback('NavigationBar', tokens, variant)
|
|
52
|
+
const getStateTokens = (state) => getTokens({ ...state, viewport })
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<ItemContainer targetWidth={targetWidth}>
|
|
56
|
+
<Button
|
|
57
|
+
accessibilityRole={accessibilityRole}
|
|
58
|
+
accessibilityState={accessibilityState}
|
|
59
|
+
onPress={handleClick}
|
|
60
|
+
ref={ref}
|
|
61
|
+
tokens={getStateTokens}
|
|
62
|
+
variant={{ selected }}
|
|
63
|
+
href={href}
|
|
64
|
+
{...selectedProps}
|
|
65
|
+
>
|
|
66
|
+
{children}
|
|
67
|
+
</Button>
|
|
68
|
+
</ItemContainer>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
NavigationItem.displayName = 'NavigationItem'
|
|
73
|
+
|
|
74
|
+
NavigationItem.propTypes = {
|
|
75
|
+
...selectedSystemPropTypes,
|
|
76
|
+
...withLinkRouter.propTypes,
|
|
77
|
+
onClick: PropTypes.func,
|
|
78
|
+
selected: PropTypes.bool,
|
|
79
|
+
children: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired
|
|
80
|
+
}
|
|
81
|
+
NavigationItem.defaultProps = { onClick: () => {}, selected: false }
|
|
82
|
+
|
|
83
|
+
export default withLinkRouter(NavigationItem)
|