@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.
Files changed (83) hide show
  1. package/CHANGELOG.md +40 -2
  2. package/lib/Breadcrumbs/Breadcrumbs.js +8 -3
  3. package/lib/Breadcrumbs/Item/Item.js +31 -6
  4. package/lib/Callout/Callout.js +24 -3
  5. package/lib/Disclaimer/Disclaimer.js +72 -0
  6. package/lib/Disclaimer/index.js +15 -0
  7. package/lib/Footnote/Footnote.js +70 -28
  8. package/lib/Footnote/FootnoteLink.js +11 -13
  9. package/lib/NavigationBar/NavigationBar.js +231 -0
  10. package/lib/NavigationBar/NavigationItem.js +111 -0
  11. package/lib/NavigationBar/NavigationSubMenu.js +179 -0
  12. package/lib/NavigationBar/collapseItems.js +51 -0
  13. package/lib/NavigationBar/index.js +13 -0
  14. package/lib/PriceLockup/PriceLockup.js +40 -17
  15. package/lib/PriceLockup/tokens.js +49 -116
  16. package/lib/Progress/ProgressBar.js +100 -0
  17. package/lib/Progress/index.js +16 -0
  18. package/lib/Ribbon/Ribbon.js +53 -32
  19. package/lib/Spinner/Spinner.js +18 -14
  20. package/lib/Table/Cell.js +15 -1
  21. package/lib/Toast/Toast.js +15 -8
  22. package/lib/VideoPicker/VideoPicker.js +177 -0
  23. package/lib/VideoPicker/VideoPickerPlayer.js +54 -0
  24. package/lib/VideoPicker/VideoPickerThumbnail.js +201 -0
  25. package/lib/VideoPicker/VideoSlider.js +100 -0
  26. package/lib/VideoPicker/index.js +13 -0
  27. package/lib/VideoPicker/videoPropType.js +25 -0
  28. package/lib/index.js +37 -1
  29. package/lib-module/Breadcrumbs/Breadcrumbs.js +8 -3
  30. package/lib-module/Breadcrumbs/Item/Item.js +32 -7
  31. package/lib-module/Callout/Callout.js +24 -3
  32. package/lib-module/Disclaimer/Disclaimer.js +54 -0
  33. package/lib-module/Disclaimer/index.js +1 -0
  34. package/lib-module/Footnote/Footnote.js +68 -27
  35. package/lib-module/Footnote/FootnoteLink.js +12 -14
  36. package/lib-module/NavigationBar/NavigationBar.js +207 -0
  37. package/lib-module/NavigationBar/NavigationItem.js +87 -0
  38. package/lib-module/NavigationBar/NavigationSubMenu.js +161 -0
  39. package/lib-module/NavigationBar/collapseItems.js +43 -0
  40. package/lib-module/NavigationBar/index.js +2 -0
  41. package/lib-module/PriceLockup/PriceLockup.js +42 -19
  42. package/lib-module/PriceLockup/tokens.js +54 -119
  43. package/lib-module/Progress/ProgressBar.js +83 -0
  44. package/lib-module/Progress/index.js +4 -0
  45. package/lib-module/Ribbon/Ribbon.js +53 -32
  46. package/lib-module/Spinner/Spinner.js +17 -14
  47. package/lib-module/Table/Cell.js +15 -1
  48. package/lib-module/Toast/Toast.js +15 -8
  49. package/lib-module/VideoPicker/VideoPicker.js +151 -0
  50. package/lib-module/VideoPicker/VideoPickerPlayer.js +41 -0
  51. package/lib-module/VideoPicker/VideoPickerThumbnail.js +180 -0
  52. package/lib-module/VideoPicker/VideoSlider.js +83 -0
  53. package/lib-module/VideoPicker/index.js +2 -0
  54. package/lib-module/VideoPicker/videoPropType.js +9 -0
  55. package/lib-module/index.js +4 -0
  56. package/package.json +3 -3
  57. package/src/Breadcrumbs/Breadcrumbs.jsx +4 -3
  58. package/src/Breadcrumbs/Item/Item.jsx +18 -4
  59. package/src/Callout/Callout.jsx +27 -3
  60. package/src/Disclaimer/Disclaimer.jsx +39 -0
  61. package/src/Disclaimer/index.js +1 -0
  62. package/src/Footnote/Footnote.jsx +76 -26
  63. package/src/Footnote/FootnoteLink.jsx +28 -18
  64. package/src/NavigationBar/NavigationBar.jsx +217 -0
  65. package/src/NavigationBar/NavigationItem.jsx +83 -0
  66. package/src/NavigationBar/NavigationSubMenu.jsx +121 -0
  67. package/src/NavigationBar/collapseItems.js +29 -0
  68. package/src/NavigationBar/index.js +3 -0
  69. package/src/PriceLockup/PriceLockup.jsx +47 -21
  70. package/src/PriceLockup/tokens.js +34 -54
  71. package/src/Progress/ProgressBar.jsx +67 -0
  72. package/src/Progress/index.js +6 -0
  73. package/src/Ribbon/Ribbon.jsx +21 -9
  74. package/src/Spinner/Spinner.jsx +20 -17
  75. package/src/Table/Cell.jsx +22 -5
  76. package/src/Toast/Toast.jsx +12 -5
  77. package/src/VideoPicker/VideoPicker.jsx +144 -0
  78. package/src/VideoPicker/VideoPickerPlayer.jsx +21 -0
  79. package/src/VideoPicker/VideoPickerThumbnail.jsx +182 -0
  80. package/src/VideoPicker/VideoSlider.jsx +85 -0
  81. package/src/VideoPicker/index.js +3 -0
  82. package/src/VideoPicker/videoPropType.js +12 -0
  83. 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 dictionary from './dictionary'
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 { copy, number, content, onClose, isOpen, tokens, variant = {}, ...rest } = props
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 listRef = useRef(null)
233
+ const contentRef = useRef(null)
220
234
  const headingRef = useRef(null)
221
235
  const [data, setData] = useState({ content: null, number: null })
222
236
  const [headerHeight, setHeaderHeight] = useState('auto')
@@ -265,7 +279,7 @@ const Footnote = (props) => {
265
279
  )
266
280
 
267
281
  const saveCurrentHeight = () => {
268
- const oldHeight = listRef.current.offsetHeight
282
+ const oldHeight = contentRef.current.offsetHeight
269
283
  setBodyHeight(oldHeight)
270
284
  }
271
285
 
@@ -287,14 +301,14 @@ const Footnote = (props) => {
287
301
  event.persist()
288
302
  if (event.propertyName === 'opacity' && !isTextVisible) {
289
303
  setData({ content, number })
290
- if (bodyHeight !== listRef.current.offsetHeight) {
304
+ if (bodyHeight !== contentRef.current.offsetHeight) {
291
305
  // Set new height
292
- setBodyHeight(listRef.current.offsetHeight)
306
+ setBodyHeight(contentRef.current.offsetHeight)
293
307
  } else {
294
308
  setIsTextVisible(true)
295
309
  }
296
310
  } else {
297
- setBodyHeight(listRef.current.offsetHeight)
311
+ setBodyHeight(contentRef.current.offsetHeight)
298
312
  }
299
313
 
300
314
  if (event.propertyName === 'height' && !isTextVisible) {
@@ -359,6 +373,41 @@ const Footnote = (props) => {
359
373
  // Reset footnote on close
360
374
  useEffect(resetFootnote, [isOpen])
361
375
 
376
+ const getFootnoteBodyContent = useCallback(() => {
377
+ if (!data.number || !data.content) {
378
+ return null
379
+ }
380
+ if (React.isValidElement(data.content)) {
381
+ return (
382
+ <StyledCustomContentContainer ref={contentRef}>{data.content}</StyledCustomContentContainer>
383
+ )
384
+ }
385
+ return (
386
+ <List start={data.number} ref={contentRef} listPaddingLeft={listPaddingLeft}>
387
+ <ListItem
388
+ listItemMarkerFontSize={listItemMarkerFontSize}
389
+ listItemMarkerLineHeight={listItemMarkerLineHeight}
390
+ listItemColor={listItemColor}
391
+ listItemFontSize={listItemFontSize}
392
+ listItemLineHeight={listItemLineHeight}
393
+ listItemPaddingLeft={listItemPaddingLeft}
394
+ >
395
+ <Typography>{renderStructuredContent(data.content)}</Typography>
396
+ </ListItem>
397
+ </List>
398
+ )
399
+ }, [
400
+ data.content,
401
+ data.number,
402
+ listItemColor,
403
+ listItemFontSize,
404
+ listItemLineHeight,
405
+ listItemMarkerFontSize,
406
+ listItemMarkerLineHeight,
407
+ listItemPaddingLeft,
408
+ listPaddingLeft
409
+ ])
410
+
362
411
  return (
363
412
  <Portal>
364
413
  <div {...selectProps(rest)}>
@@ -388,7 +437,7 @@ const Footnote = (props) => {
388
437
  }}
389
438
  aria-label={getCopy('close')}
390
439
  >
391
- <Icon icon={Close} tokens={{ size: `${closeButtonIconSize}px` }} />
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
- {data.number && data.content && (
406
- <List start={data.number} ref={listRef} listPaddingLeft={listPaddingLeft}>
407
- <ListItem
408
- listItemMarkerFontSize={listItemMarkerFontSize}
409
- listItemMarkerLineHeight={listItemMarkerLineHeight}
410
- listItemColor={listItemColor}
411
- listItemFontSize={listItemFontSize}
412
- listItemLineHeight={listItemLineHeight}
413
- listItemPaddingLeft={listItemPaddingLeft}
414
- >
415
- <Typography>{renderStructuredContent(data.content)}</Typography>
416
- </ListItem>
417
- </List>
418
- )}
454
+ {getFootnoteBodyContent()}
419
455
  </StyledFootnoteBody>
420
456
  </ContentContainer>
421
457
  </StyledFootnote>
@@ -429,12 +465,19 @@ const copyShape = PropTypes.shape({
429
465
  heading: PropTypes.string.isRequired
430
466
  })
431
467
 
468
+ // If a language dictionary entry is provided, it must contain every key
469
+ const dictionaryContentShape = PropTypes.shape({
470
+ a11yLabel: PropTypes.string.isRequired,
471
+ close: PropTypes.string.isRequired,
472
+ heading: PropTypes.string.isRequired
473
+ })
474
+
432
475
  Footnote.propTypes = {
433
476
  ...selectedSystemPropTypes,
434
477
  /**
435
478
  * The content.
436
479
  */
437
- content: PropTypes.string,
480
+ content: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
438
481
  /**
439
482
  * Use the `copy` prop to either select provided English or French copy by passing 'en' or 'fr' respectively.
440
483
  * To provide your own, pass a JSON object with the keys `heading` and `close`.
@@ -455,7 +498,14 @@ Footnote.propTypes = {
455
498
  * @param {Object} options Custom options
456
499
  * @param {boolean} options.returnFocus Should the `Footnote` return focus on close
457
500
  */
458
- onClose: PropTypes.func.isRequired
501
+ onClose: PropTypes.func.isRequired,
502
+ /**
503
+ * Override the default dictionary, by passing the complete dictionary object for `en` and `fr`
504
+ */
505
+ dictionary: PropTypes.shape({
506
+ en: dictionaryContentShape,
507
+ fr: dictionaryContentShape
508
+ })
459
509
  }
460
510
 
461
511
  Footnote.defaultProps = {
@@ -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 { selectSystemProps, useCopy, useThemeTokens } from '@telus-uds/components-base'
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(({ 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
- }))
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 { lineHeight, paddingLeft, paddingRight } = useThemeTokens('FootnoteLink', tokens, variant)
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)