@telus-uds/components-base 3.13.0 → 3.14.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 (36) hide show
  1. package/CHANGELOG.md +19 -2
  2. package/lib/cjs/BaseProvider/index.js +4 -1
  3. package/lib/cjs/Card/Card.js +23 -4
  4. package/lib/cjs/Card/CardBase.js +170 -19
  5. package/lib/cjs/Card/PressableCardBase.js +19 -5
  6. package/lib/cjs/Card/backgroundImageStylesMap.js +197 -0
  7. package/lib/cjs/FlexGrid/FlexGrid.js +29 -13
  8. package/lib/cjs/Tabs/Tabs.js +34 -2
  9. package/lib/cjs/Tabs/TabsDropdown.js +252 -0
  10. package/lib/cjs/Tabs/TabsItem.js +4 -2
  11. package/lib/cjs/Tabs/dictionary.js +14 -0
  12. package/lib/cjs/ViewportProvider/ViewportProvider.js +9 -3
  13. package/lib/esm/BaseProvider/index.js +4 -1
  14. package/lib/esm/Card/Card.js +21 -4
  15. package/lib/esm/Card/CardBase.js +169 -19
  16. package/lib/esm/Card/PressableCardBase.js +19 -5
  17. package/lib/esm/Card/backgroundImageStylesMap.js +190 -0
  18. package/lib/esm/FlexGrid/FlexGrid.js +29 -13
  19. package/lib/esm/Tabs/Tabs.js +35 -3
  20. package/lib/esm/Tabs/TabsDropdown.js +245 -0
  21. package/lib/esm/Tabs/TabsItem.js +4 -2
  22. package/lib/esm/Tabs/dictionary.js +8 -0
  23. package/lib/esm/ViewportProvider/ViewportProvider.js +9 -3
  24. package/lib/package.json +2 -2
  25. package/package.json +2 -2
  26. package/src/BaseProvider/index.jsx +4 -2
  27. package/src/Card/Card.jsx +27 -3
  28. package/src/Card/CardBase.jsx +165 -19
  29. package/src/Card/PressableCardBase.jsx +31 -4
  30. package/src/Card/backgroundImageStylesMap.js +41 -0
  31. package/src/FlexGrid/FlexGrid.jsx +31 -14
  32. package/src/Tabs/Tabs.jsx +36 -2
  33. package/src/Tabs/TabsDropdown.jsx +265 -0
  34. package/src/Tabs/TabsItem.jsx +4 -2
  35. package/src/Tabs/dictionary.js +8 -0
  36. package/src/ViewportProvider/ViewportProvider.jsx +8 -3
@@ -80,6 +80,7 @@ const PressableCardBase = React.forwardRef(
80
80
  href,
81
81
  hrefAttrs,
82
82
  dataSet,
83
+ backgroundImage,
83
84
  accessibilityRole = href ? 'link' : undefined,
84
85
  ...rawRest
85
86
  },
@@ -158,10 +159,13 @@ const PressableCardBase = React.forwardRef(
158
159
  setFocused(false)
159
160
  setPressed(false)
160
161
  }}
161
- style={{ ...staticStyles.container, textDecoration: 'none' }}
162
+ style={staticStyles.linkContainer}
162
163
  {...(hrefAttrs || {})}
163
164
  >
164
- <CardBase tokens={getCardTokens({ pressed, focused, hovered })}>
165
+ <CardBase
166
+ tokens={getCardTokens({ pressed, focused, hovered })}
167
+ backgroundImage={backgroundImage}
168
+ >
165
169
  {typeof children === 'function'
166
170
  ? children(getCardState({ pressed, focused, hovered }))
167
171
  : children}
@@ -183,7 +187,7 @@ const PressableCardBase = React.forwardRef(
183
187
  {...selectProps({ ...rest, accessibilityRole })}
184
188
  >
185
189
  {(pressableState) => (
186
- <CardBase tokens={getCardTokens(pressableState)}>
190
+ <CardBase tokens={getCardTokens(pressableState)} backgroundImage={backgroundImage}>
187
191
  {typeof children === 'function' ? children(getCardState(pressableState)) : children}
188
192
  </CardBase>
189
193
  )}
@@ -196,6 +200,11 @@ const staticStyles = StyleSheet.create({
196
200
  container: {
197
201
  flex: 1,
198
202
  display: 'flex'
203
+ },
204
+ linkContainer: {
205
+ flex: 1,
206
+ display: 'flex',
207
+ textDecoration: 'none'
199
208
  }
200
209
  })
201
210
 
@@ -205,7 +214,25 @@ PressableCardBase.propTypes = {
205
214
  ...selectedSystemPropTypes,
206
215
  children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
207
216
  tokens: getTokensSetPropType(tokenKeys, { partial: true, allowFunction: true }),
208
- variant: variantProp.propType
217
+ variant: variantProp.propType,
218
+ backgroundImage: PropTypes.shape({
219
+ // The image src is either a URI string or a number (when a local image src is bundled in IOS or Android app)
220
+ // src is an object when used responsively to provide different image sources for different screen sizes
221
+ src: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object]).isRequired,
222
+ alt: PropTypes.string,
223
+ resizeMode: PropTypes.oneOfType([
224
+ PropTypes.oneOf(['cover', 'contain', 'stretch', 'repeat', 'center']),
225
+ PropTypes.object
226
+ ]),
227
+ position: PropTypes.oneOfType([
228
+ PropTypes.oneOf(['bottom', 'left', 'right', 'top']),
229
+ PropTypes.object
230
+ ]),
231
+ align: PropTypes.oneOfType([
232
+ PropTypes.oneOf(['start', 'end', 'center', 'stretch']),
233
+ PropTypes.object
234
+ ])
235
+ })
209
236
  }
210
237
 
211
238
  export default withLinkRouter(PressableCardBase)
@@ -0,0 +1,41 @@
1
+ import { Platform } from 'react-native'
2
+
3
+ const webStyles = {
4
+ 'top-start': { top: 0, left: 0 },
5
+ 'top-center': { top: 0, left: '50%', transform: [{ translateX: '-50%' }] },
6
+ 'top-end': { top: 0, right: 0 },
7
+ 'right-start': { top: 0, right: 0 },
8
+ 'left-start': { top: 0, left: 0 },
9
+ 'left-center': { top: '50%', left: 0, transform: [{ translateY: '-50%' }] },
10
+ 'right-center': { top: '50%', right: 0, transform: [{ translateY: '-50%' }] },
11
+ 'bottom-start': { bottom: 0, left: 0 },
12
+ 'left-end': { bottom: 0, left: 0 },
13
+ 'bottom-center': { bottom: 0, left: '50%', transform: [{ translateX: '-50%' }] },
14
+ 'bottom-end': { bottom: 0, right: 0 },
15
+ 'right-end': { bottom: 0, right: 0 },
16
+ 'top-stretch': { top: 0, left: 0, right: 0, width: '100%' },
17
+ 'left-stretch': { top: 0, bottom: 0, left: 0, height: '100%' },
18
+ 'right-stretch': { top: 0, bottom: 0, right: 0, height: '100%' },
19
+ 'bottom-stretch': { bottom: 0, left: 0, right: 0, width: '100%' }
20
+ }
21
+
22
+ const nativeStyles = {
23
+ 'top-start': { top: 0, left: 0, width: 150, height: 200 },
24
+ 'top-center': { top: 0, left: '50%', marginLeft: -75, width: 150, height: 200 },
25
+ 'top-end': { top: 0, right: 0, width: 150, height: 200 },
26
+ 'right-start': { top: 0, right: 0, width: 150, height: 200 },
27
+ 'left-start': { top: 0, left: 0, width: 150, height: 200 },
28
+ 'left-center': { left: 0, top: '50%', marginTop: -100, width: 150, height: 200 },
29
+ 'right-center': { right: 0, top: '50%', marginTop: -100, width: 150, height: 200 },
30
+ 'bottom-start': { bottom: 0, left: 0, width: 150, height: 200 },
31
+ 'left-end': { bottom: 0, left: 0, width: 150, height: 200 },
32
+ 'bottom-center': { bottom: 0, left: '50%', marginLeft: -75, width: 150, height: 200 },
33
+ 'bottom-end': { bottom: 0, right: 0, width: 150, height: 200 },
34
+ 'right-end': { bottom: 0, right: 0, width: 150, height: 200 },
35
+ 'top-stretch': { top: 0, left: 0, right: 0, width: '100%' },
36
+ 'left-stretch': { top: 0, bottom: 0, left: 0, height: '100%' },
37
+ 'right-stretch': { top: 0, bottom: 0, right: 0, height: '100%' },
38
+ 'bottom-stretch': { bottom: 0, left: 0, right: 0, width: '100%' }
39
+ }
40
+
41
+ export default Platform.OS === 'web' ? webStyles : nativeStyles
@@ -29,19 +29,15 @@ const CONTENT_FULL_WIDTH = 'full'
29
29
  * Resolves the maximum width for content based on the provided value and responsive width.
30
30
  *
31
31
  * @param {number|string|null|undefined} contentMinWidthValue - The minimum width value for the content.
32
- * Can be a number (pixels), a string constant (e.g., CONTENT_FULL_WIDTH, CONTENT_MAX_WIDTH), or null/undefined.
33
- * @param {number|string} responsiveWidth - The responsive width to use when contentMinWidthValue is CONTENT_MAX_WIDTH.
34
- * @returns {number|string|null} The resolved maximum width value, which can be a number, a string (e.g., '100%'), or null.
32
+ * Can be a number, a special string constant (e.g., CONTENT_FULL_WIDTH, CONTENT_MAX_WIDTH), or null/undefined.
33
+ * @param {number} responsiveWidth - The responsive width to use when contentMinWidthValue is CONTENT_MAX_WIDTH.
34
+ * @returns {number|string|null} The resolved maximum width value, or null if full width is desired.
35
35
  */
36
36
  const resolveContentMaxWidth = (contentMinWidthValue, responsiveWidth) => {
37
- if (!contentMinWidthValue) {
37
+ if (!contentMinWidthValue || contentMinWidthValue === CONTENT_FULL_WIDTH) {
38
38
  return null
39
39
  }
40
40
 
41
- if (contentMinWidthValue === CONTENT_FULL_WIDTH) {
42
- return '100%'
43
- }
44
-
45
41
  if (Number.isFinite(contentMinWidthValue)) {
46
42
  return contentMinWidthValue
47
43
  }
@@ -53,6 +49,27 @@ const resolveContentMaxWidth = (contentMinWidthValue, responsiveWidth) => {
53
49
  return contentMinWidthValue
54
50
  }
55
51
 
52
+ /**
53
+ * Calculates the maximum width for a given viewport based on limitWidth and contentMinWidth settings.
54
+ *
55
+ * @param {string} viewportKey - The viewport key ('xs', 'sm', 'md', 'lg', 'xl')
56
+ * @param {boolean} limitWidth - Whether to limit the width to viewport breakpoints
57
+ * @param {any} contentMinWidth - The contentMinWidth prop value
58
+ * @param {number|string|null} maxWidth - The resolved max width value
59
+ * @returns {number|string|null} The calculated maximum width for the viewport
60
+ */
61
+ const getMaxWidthForViewport = (viewportKey, limitWidth, contentMinWidth, maxWidth) => {
62
+ if (limitWidth) {
63
+ return viewports.map.get(viewportKey === 'xs' ? 'sm' : viewportKey)
64
+ }
65
+
66
+ if (contentMinWidth) {
67
+ return maxWidth
68
+ }
69
+
70
+ return viewportKey === 'xl' ? viewports.map.get('xl') : null
71
+ }
72
+
56
73
  /**
57
74
  * A mobile-first flexbox grid.
58
75
  */
@@ -60,7 +77,7 @@ const resolveContentMaxWidth = (contentMinWidthValue, responsiveWidth) => {
60
77
  const FlexGrid = React.forwardRef(
61
78
  (
62
79
  {
63
- limitWidth = true,
80
+ limitWidth = false,
64
81
  gutter = true,
65
82
  outsideGutter = true,
66
83
  xsReverse,
@@ -93,23 +110,23 @@ const FlexGrid = React.forwardRef(
93
110
 
94
111
  const stylesByViewport = {
95
112
  xs: {
96
- maxWidth: limitWidth ? viewports.map.get('sm') : maxWidth,
113
+ maxWidth: getMaxWidthForViewport('xs', limitWidth, contentMinWidth, maxWidth),
97
114
  flexDirection: reverseLevel[0] ? 'column-reverse' : 'column'
98
115
  },
99
116
  sm: {
100
- maxWidth: limitWidth ? viewports.map.get('sm') : maxWidth,
117
+ maxWidth: getMaxWidthForViewport('sm', limitWidth, contentMinWidth, maxWidth),
101
118
  flexDirection: reverseLevel[1] ? 'column-reverse' : 'column'
102
119
  },
103
120
  md: {
104
- maxWidth: limitWidth ? viewports.map.get('md') : maxWidth,
121
+ maxWidth: getMaxWidthForViewport('md', limitWidth, contentMinWidth, maxWidth),
105
122
  flexDirection: reverseLevel[2] ? 'column-reverse' : 'column'
106
123
  },
107
124
  lg: {
108
- maxWidth: limitWidth ? viewports.map.get('lg') : maxWidth,
125
+ maxWidth: getMaxWidthForViewport('lg', limitWidth, contentMinWidth, maxWidth),
109
126
  flexDirection: reverseLevel[3] ? 'column-reverse' : 'column'
110
127
  },
111
128
  xl: {
112
- maxWidth: limitWidth ? viewports.map.get('xl') : maxWidth,
129
+ maxWidth: getMaxWidthForViewport('xl', limitWidth, contentMinWidth, maxWidth),
113
130
  flexDirection: reverseLevel[4] ? 'column-reverse' : 'column'
114
131
  }
115
132
  }
package/src/Tabs/Tabs.jsx CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  selectSystemProps,
13
13
  useHash,
14
14
  useInputValue,
15
+ useResponsiveProp,
15
16
  variantProp,
16
17
  viewProps,
17
18
  withLinkRouter
@@ -21,6 +22,7 @@ import HorizontalScroll, {
21
22
  HorizontalScrollButton
22
23
  } from '../HorizontalScroll'
23
24
  import TabsItem from './TabsItem'
25
+ import TabsDropdown from './TabsDropdown'
24
26
 
25
27
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
26
28
  const [selectItemProps, selectedItemPropTypes] = selectSystemProps([
@@ -58,6 +60,12 @@ const getStackViewTokens = (variant) => {
58
60
  * Tabs renders a horizontally-scrolling menu of selectable buttons which may link
59
61
  * to a page or control what content is displayed on this page.
60
62
  *
63
+ * By default, Tabs always renders as horizontal scrolling tabs regardless of viewport.
64
+ * To enable dropdown mode, you must explicitly pass `variant={{ dropdown: true }}`.
65
+ * When dropdown is enabled, it will only render as a dropdown on mobile and tablet
66
+ * viewports (XS and SM). On larger viewports (MD, LG, XL), it will still render as
67
+ * horizontal tabs even with dropdown enabled.
68
+ *
61
69
  * If you are using Tabs to navigate to a new page (web-only) you should pass
62
70
  * `navigation`as the `accessibilityRole` to te Tabs component, this will cause
63
71
  * TabItems to default to a role of link and obtain aria-current behaviour.
@@ -102,6 +110,32 @@ const Tabs = React.forwardRef(
102
110
  getDefaultTabItemAccessibilityRole(parentAccessibilityRole)
103
111
  const stackViewTokens = getStackViewTokens(variant)
104
112
 
113
+ // Render dropdown only if explicitly requested via variant AND viewport is xs or sm
114
+ const isSmallViewport = useResponsiveProp(
115
+ { xs: true, sm: true, md: false, lg: false, xl: false },
116
+ false
117
+ )
118
+ const shouldRenderDropdown = variant?.dropdown === true && isSmallViewport
119
+
120
+ if (shouldRenderDropdown) {
121
+ return (
122
+ <TabsDropdown
123
+ ref={ref}
124
+ tokens={tokens}
125
+ itemTokens={itemTokens}
126
+ variant={variant}
127
+ value={currentValue}
128
+ onChange={setValue}
129
+ items={items}
130
+ LinkRouter={LinkRouter}
131
+ linkRouterProps={linkRouterProps}
132
+ accessibilityRole={
133
+ parentAccessibilityRole === 'tablist' ? 'button' : parentAccessibilityRole
134
+ }
135
+ {...restProps}
136
+ />
137
+ )
138
+ }
105
139
  return (
106
140
  <HorizontalScroll
107
141
  ref={ref}
@@ -169,14 +203,14 @@ Tabs.displayName = 'Tabs'
169
203
 
170
204
  Tabs.propTypes = {
171
205
  ...selectedSystemPropTypes,
172
- ...withLinkRouter.PropTypes,
206
+ ...withLinkRouter.propTypes,
173
207
  /**
174
208
  * Array of `TabsItem`s
175
209
  */
176
210
  items: PropTypes.arrayOf(
177
211
  PropTypes.shape({
178
212
  ...selectedItemPropTypes,
179
- ...withLinkRouter.PropTypes,
213
+ ...withLinkRouter.propTypes,
180
214
  href: PropTypes.string,
181
215
  label: PropTypes.string,
182
216
  id: PropTypes.string,
@@ -0,0 +1,265 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import { Pressable, StyleSheet, Text, View } from 'react-native'
4
+ import { useThemeTokensCallback, applyTextStyles, useTheme } from '../ThemeProvider'
5
+ import {
6
+ a11yProps,
7
+ getTokensPropType,
8
+ resolvePressableTokens,
9
+ selectSystemProps,
10
+ selectTokens,
11
+ useOverlaidPosition,
12
+ useCopy,
13
+ variantProp,
14
+ viewProps,
15
+ withLinkRouter
16
+ } from '../utils'
17
+ import { useViewport } from '../ViewportProvider'
18
+ import Icon from '../Icon'
19
+ import Listbox from '../Listbox'
20
+ import dictionary from './dictionary'
21
+
22
+ const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
23
+
24
+ const selectButtonContentStyles = ({
25
+ backgroundColor,
26
+ borderColor,
27
+ borderWidth,
28
+ borderRadius,
29
+ paddingHorizontal,
30
+ paddingVertical,
31
+ marginHorizontal,
32
+ marginVertical
33
+ }) => ({
34
+ backgroundColor,
35
+ borderColor,
36
+ borderWidth,
37
+ borderRadius,
38
+ paddingHorizontal,
39
+ paddingVertical,
40
+ marginLeft: marginHorizontal,
41
+ marginRight: marginHorizontal,
42
+ marginTop: marginVertical,
43
+ marginBottom: marginVertical
44
+ })
45
+
46
+ /**
47
+ * TabsDropdown renders a dropdown version of tabs for mobile/tablet viewports.
48
+ * It shows the currently selected tab as a button that opens a dropdown menu
49
+ * containing all available tabs.
50
+ *
51
+ * This is rendered automatically by `Tabs` on mobile viewports and when variant
52
+ * is dropdown and isn't intended to be used directly.
53
+ */
54
+ const TabsDropdown = React.forwardRef(
55
+ (
56
+ {
57
+ itemTokens,
58
+ variant,
59
+ value,
60
+ onChange,
61
+ items = [],
62
+ LinkRouter,
63
+ linkRouterProps,
64
+ accessibilityRole = 'button',
65
+ copy = 'en',
66
+ dictionary: customDictionary = dictionary,
67
+ ...rest
68
+ },
69
+ ref
70
+ ) => {
71
+ const { themeOptions } = useTheme()
72
+ const viewport = useViewport()
73
+ const [isOpen, setIsOpen] = React.useState(false)
74
+
75
+ const getTokens = useThemeTokensCallback('TabsItem', itemTokens, { viewport, ...variant })
76
+
77
+ const selectedItem =
78
+ items.find((item) => {
79
+ const itemId = item.id ?? item.label
80
+ return value === itemId
81
+ }) || items[0]
82
+
83
+ const { overlaidPosition, sourceRef, targetRef, onTargetLayout, isReady } = useOverlaidPosition(
84
+ {
85
+ isShown: isOpen,
86
+ offsets: { vertical: 4 },
87
+ align: { top: 'bottom', left: 'left' }
88
+ }
89
+ )
90
+
91
+ const handleToggle = () => setIsOpen((prev) => !prev)
92
+ const handleClose = () => setIsOpen(false)
93
+
94
+ const handleItemSelect = (item, event) => {
95
+ const itemId = item.id ?? item.label
96
+ setIsOpen(false)
97
+
98
+ if (onChange) onChange(itemId, event)
99
+ if (item.onPress) item.onPress(event)
100
+ }
101
+
102
+ const listboxItems = items.map((item) => ({
103
+ ...item,
104
+ onPress: (event) => handleItemSelect(item, event)
105
+ }))
106
+
107
+ const isSelected = Boolean(selectedItem && value)
108
+ const getCopy = useCopy({ dictionary: customDictionary, copy })
109
+
110
+ const selectedProps = selectProps({
111
+ accessibilityRole,
112
+ ...rest
113
+ })
114
+
115
+ return (
116
+ <View ref={ref} style={styles.container}>
117
+ <Pressable
118
+ ref={sourceRef}
119
+ onPress={handleToggle}
120
+ {...selectedProps}
121
+ style={styles.pressable}
122
+ >
123
+ {(pressableState) => {
124
+ // Use resolvePressableTokens like TabBarItem does for proper state handling
125
+ const resolvedTokens = resolvePressableTokens(getTokens, pressableState, {
126
+ viewport,
127
+ expanded: isOpen,
128
+ selected: isSelected
129
+ })
130
+
131
+ const textStyles = applyTextStyles({
132
+ ...selectTokens('Typography', resolvedTokens),
133
+ themeOptions
134
+ })
135
+
136
+ // Get dropdown icons from resolved tokens
137
+ const dropdownIcon = isOpen
138
+ ? resolvedTokens.dropdownIconExpanded
139
+ : resolvedTokens.dropdownIcon
140
+
141
+ return (
142
+ <View style={[styles.buttonContent, selectButtonContentStyles(resolvedTokens)]}>
143
+ <Text style={textStyles}>{selectedItem?.label || getCopy('selectTab')}</Text>
144
+ {dropdownIcon && (
145
+ <Icon
146
+ icon={dropdownIcon}
147
+ variant={{ size: 'micro' }}
148
+ tokens={{
149
+ color: textStyles.color
150
+ }}
151
+ />
152
+ )}
153
+ </View>
154
+ )
155
+ }}
156
+ </Pressable>
157
+
158
+ {isOpen && (
159
+ <Listbox.Overlay
160
+ overlaidPosition={overlaidPosition}
161
+ maxWidth={400}
162
+ minWidth={200}
163
+ isReady={isReady}
164
+ onLayout={onTargetLayout}
165
+ >
166
+ <Listbox
167
+ items={listboxItems}
168
+ firstItemRef={targetRef}
169
+ parentRef={sourceRef}
170
+ selectedId={value}
171
+ onClose={handleClose}
172
+ LinkRouter={LinkRouter}
173
+ linkRouterProps={linkRouterProps}
174
+ />
175
+ </Listbox.Overlay>
176
+ )}
177
+ </View>
178
+ )
179
+ }
180
+ )
181
+
182
+ TabsDropdown.displayName = 'TabsDropdown'
183
+
184
+ const dictionaryContentShape = PropTypes.shape({
185
+ selectTab: PropTypes.string.isRequired
186
+ })
187
+
188
+ TabsDropdown.propTypes = {
189
+ ...selectedSystemPropTypes,
190
+ ...withLinkRouter.propTypes,
191
+ /**
192
+ * Array of tab items
193
+ */
194
+ items: PropTypes.arrayOf(
195
+ PropTypes.shape({
196
+ ...withLinkRouter.propTypes,
197
+ /** URL to navigate to when the tab is pressed */
198
+ href: PropTypes.string,
199
+ /** Display text for the tab */
200
+ label: PropTypes.string,
201
+ /** Unique identifier for the tab */
202
+ id: PropTypes.string,
203
+ /** Reference to the tab element */
204
+ ref: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
205
+ /** Custom render function for the tab content */
206
+ render: PropTypes.func
207
+ })
208
+ ),
209
+ /**
210
+ * Current selected tab id
211
+ */
212
+ value: PropTypes.string,
213
+ /**
214
+ * Callback for when the selected tab changes
215
+ */
216
+ onChange: PropTypes.func,
217
+ /**
218
+ * Custom tokens for the main Tabs container
219
+ */
220
+ tokens: getTokensPropType('Tabs'),
221
+ /**
222
+ * Custom tokens for `TabsItem`
223
+ */
224
+ itemTokens: getTokensPropType('TabsItem'),
225
+ /**
226
+ * Visual and behavioral variants for the tabs dropdown
227
+ */
228
+ variant: variantProp.propType,
229
+ /**
230
+ * Select English or French copy for the accessible labels.
231
+ * You may also pass in a custom dictionary object.
232
+ */
233
+ copy: PropTypes.oneOfType([PropTypes.oneOf(['en', 'fr']), dictionaryContentShape]),
234
+ /**
235
+ * Override the default dictionary, by passing the complete dictionary object for `en` and `fr`
236
+ */
237
+ dictionary: PropTypes.shape({
238
+ en: dictionaryContentShape,
239
+ fr: dictionaryContentShape
240
+ })
241
+ }
242
+
243
+ const styles = StyleSheet.create({
244
+ container: {
245
+ position: 'relative',
246
+ width: '100%'
247
+ },
248
+ pressable: {
249
+ outlineWidth: 0,
250
+ outlineStyle: 'none',
251
+ outlineColor: 'transparent'
252
+ },
253
+ buttonContent: {
254
+ display: 'flex',
255
+ flexDirection: 'row',
256
+ alignItems: 'center',
257
+ justifyContent: 'space-between',
258
+ width: '100%',
259
+ minHeight: 44,
260
+ outline: 'none',
261
+ boxSizing: 'border-box'
262
+ }
263
+ })
264
+
265
+ export default TabsDropdown
@@ -70,8 +70,10 @@ const selectContainerStyles = ({
70
70
  borderRadius,
71
71
  paddingHorizontal: paddingHorizontal - borderWidth,
72
72
  paddingVertical: paddingVertical - borderWidth,
73
- marginHorizontal,
74
- marginVertical
73
+ marginLeft: marginHorizontal,
74
+ marginRight: marginHorizontal,
75
+ marginTop: marginVertical,
76
+ marginBottom: marginVertical
75
77
  })
76
78
 
77
79
  /**
@@ -0,0 +1,8 @@
1
+ export default {
2
+ en: {
3
+ selectTab: 'Select tab'
4
+ },
5
+ fr: {
6
+ selectTab: 'Sélectionner un onglet'
7
+ }
8
+ }
@@ -6,16 +6,21 @@ import useViewportListener from './useViewportListener'
6
6
 
7
7
  /**
8
8
  * Provides an up-to-date viewport value from system-constants, available via the `useViewport` hook
9
+ *
10
+ * @param {React.ReactNode} children - Child components that will have access to viewport context
11
+ * @param {string} [defaultViewport] - Default viewport to use during server-side rendering.
12
+ * Must be one of the viewport keys from system-constants. If not provided, defaults to the smallest viewport.
9
13
  */
10
- const ViewportProvider = ({ children }) => {
14
+ const ViewportProvider = ({ children, defaultViewport }) => {
11
15
  // Default to the smallest viewport for mobile-first SSR. On client side, this is updated
12
16
  // by useViewportListener in a layout effect before anything is shown to the user.
13
- const [viewport, setViewport] = React.useState(viewports.keys[0])
17
+ const [viewport, setViewport] = React.useState(defaultViewport || viewports.keys[0])
14
18
  useViewportListener(setViewport)
15
19
  return <ViewportContext.Provider value={viewport}>{children}</ViewportContext.Provider>
16
20
  }
17
21
  ViewportProvider.propTypes = {
18
- children: PropTypes.node.isRequired
22
+ children: PropTypes.node.isRequired,
23
+ defaultViewport: PropTypes.oneOf(viewports.keys)
19
24
  }
20
25
 
21
26
  export default ViewportProvider