@telus-uds/components-base 3.13.0 → 3.14.1
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 +10 -2
- package/lib/cjs/BaseProvider/index.js +4 -1
- package/lib/cjs/Card/Card.js +23 -4
- package/lib/cjs/Card/CardBase.js +170 -19
- package/lib/cjs/Card/PressableCardBase.js +19 -5
- package/lib/cjs/Card/backgroundImageStylesMap.js +197 -0
- package/lib/cjs/FlexGrid/FlexGrid.js +28 -12
- package/lib/cjs/Tabs/Tabs.js +34 -2
- package/lib/cjs/Tabs/TabsDropdown.js +252 -0
- package/lib/cjs/Tabs/TabsItem.js +4 -2
- package/lib/cjs/Tabs/dictionary.js +14 -0
- package/lib/cjs/ViewportProvider/ViewportProvider.js +9 -3
- package/lib/esm/BaseProvider/index.js +4 -1
- package/lib/esm/Card/Card.js +21 -4
- package/lib/esm/Card/CardBase.js +169 -19
- package/lib/esm/Card/PressableCardBase.js +19 -5
- package/lib/esm/Card/backgroundImageStylesMap.js +190 -0
- package/lib/esm/FlexGrid/FlexGrid.js +28 -12
- package/lib/esm/Tabs/Tabs.js +35 -3
- package/lib/esm/Tabs/TabsDropdown.js +245 -0
- package/lib/esm/Tabs/TabsItem.js +4 -2
- package/lib/esm/Tabs/dictionary.js +8 -0
- package/lib/esm/ViewportProvider/ViewportProvider.js +9 -3
- package/lib/package.json +2 -2
- package/package.json +2 -2
- package/src/BaseProvider/index.jsx +4 -2
- package/src/Card/Card.jsx +27 -3
- package/src/Card/CardBase.jsx +165 -19
- package/src/Card/PressableCardBase.jsx +31 -4
- package/src/Card/backgroundImageStylesMap.js +41 -0
- package/src/FlexGrid/FlexGrid.jsx +30 -13
- package/src/Tabs/Tabs.jsx +36 -2
- package/src/Tabs/TabsDropdown.jsx +265 -0
- package/src/Tabs/TabsItem.jsx +4 -2
- package/src/Tabs/dictionary.js +8 -0
- 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={
|
|
162
|
+
style={staticStyles.linkContainer}
|
|
162
163
|
{...(hrefAttrs || {})}
|
|
163
164
|
>
|
|
164
|
-
<CardBase
|
|
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
|
|
33
|
-
* @param {number
|
|
34
|
-
* @returns {number|string|null} The resolved maximum width value,
|
|
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
|
*/
|
|
@@ -93,23 +110,23 @@ const FlexGrid = React.forwardRef(
|
|
|
93
110
|
|
|
94
111
|
const stylesByViewport = {
|
|
95
112
|
xs: {
|
|
96
|
-
maxWidth:
|
|
113
|
+
maxWidth: getMaxWidthForViewport('xs', limitWidth, contentMinWidth, maxWidth),
|
|
97
114
|
flexDirection: reverseLevel[0] ? 'column-reverse' : 'column'
|
|
98
115
|
},
|
|
99
116
|
sm: {
|
|
100
|
-
maxWidth:
|
|
117
|
+
maxWidth: getMaxWidthForViewport('sm', limitWidth, contentMinWidth, maxWidth),
|
|
101
118
|
flexDirection: reverseLevel[1] ? 'column-reverse' : 'column'
|
|
102
119
|
},
|
|
103
120
|
md: {
|
|
104
|
-
maxWidth:
|
|
121
|
+
maxWidth: getMaxWidthForViewport('md', limitWidth, contentMinWidth, maxWidth),
|
|
105
122
|
flexDirection: reverseLevel[2] ? 'column-reverse' : 'column'
|
|
106
123
|
},
|
|
107
124
|
lg: {
|
|
108
|
-
maxWidth:
|
|
125
|
+
maxWidth: getMaxWidthForViewport('lg', limitWidth, contentMinWidth, maxWidth),
|
|
109
126
|
flexDirection: reverseLevel[3] ? 'column-reverse' : 'column'
|
|
110
127
|
},
|
|
111
128
|
xl: {
|
|
112
|
-
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.
|
|
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.
|
|
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
|
package/src/Tabs/TabsItem.jsx
CHANGED
|
@@ -70,8 +70,10 @@ const selectContainerStyles = ({
|
|
|
70
70
|
borderRadius,
|
|
71
71
|
paddingHorizontal: paddingHorizontal - borderWidth,
|
|
72
72
|
paddingVertical: paddingVertical - borderWidth,
|
|
73
|
-
marginHorizontal,
|
|
74
|
-
|
|
73
|
+
marginLeft: marginHorizontal,
|
|
74
|
+
marginRight: marginHorizontal,
|
|
75
|
+
marginTop: marginVertical,
|
|
76
|
+
marginBottom: marginVertical
|
|
75
77
|
})
|
|
76
78
|
|
|
77
79
|
/**
|
|
@@ -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
|