@telus-uds/components-base 3.23.0 → 3.24.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 +12 -1
- package/lib/cjs/Card/CardBase.js +97 -17
- package/lib/cjs/Card/PressableCardBase.js +12 -8
- package/lib/cjs/HorizontalScroll/HorizontalScroll.js +5 -2
- package/lib/cjs/Icon/Icon.js +3 -0
- package/lib/cjs/Listbox/GroupControl.js +12 -6
- package/lib/cjs/Listbox/Listbox.js +41 -7
- package/lib/cjs/Listbox/ListboxGroup.js +139 -8
- package/lib/cjs/Listbox/ListboxOverlay.js +10 -5
- package/lib/cjs/Listbox/SecondLevelHeader.js +201 -0
- package/lib/cjs/Listbox/dictionary.js +14 -0
- package/lib/cjs/Shortcuts/Shortcuts.js +169 -0
- package/lib/cjs/Shortcuts/ShortcutsItem.js +280 -0
- package/lib/cjs/Shortcuts/index.js +16 -0
- package/lib/cjs/Tooltip/Tooltip.native.js +2 -0
- package/lib/cjs/index.js +15 -0
- package/lib/esm/Card/CardBase.js +97 -17
- package/lib/esm/Card/PressableCardBase.js +10 -8
- package/lib/esm/HorizontalScroll/HorizontalScroll.js +6 -3
- package/lib/esm/Icon/Icon.js +3 -0
- package/lib/esm/Listbox/GroupControl.js +12 -6
- package/lib/esm/Listbox/Listbox.js +41 -7
- package/lib/esm/Listbox/ListboxGroup.js +141 -10
- package/lib/esm/Listbox/ListboxOverlay.js +10 -5
- package/lib/esm/Listbox/SecondLevelHeader.js +194 -0
- package/lib/esm/Listbox/dictionary.js +8 -0
- package/lib/esm/Shortcuts/Shortcuts.js +160 -0
- package/lib/esm/Shortcuts/ShortcutsItem.js +273 -0
- package/lib/esm/Shortcuts/index.js +3 -0
- package/lib/esm/Tooltip/Tooltip.native.js +2 -0
- package/lib/esm/index.js +1 -0
- package/lib/package.json +2 -2
- package/package.json +2 -2
- package/src/Card/CardBase.jsx +113 -14
- package/src/Card/PressableCardBase.jsx +17 -5
- package/src/HorizontalScroll/HorizontalScroll.jsx +6 -3
- package/src/Icon/Icon.jsx +3 -0
- package/src/Listbox/GroupControl.jsx +41 -33
- package/src/Listbox/Listbox.jsx +41 -2
- package/src/Listbox/ListboxGroup.jsx +158 -26
- package/src/Listbox/ListboxOverlay.jsx +18 -5
- package/src/Listbox/SecondLevelHeader.jsx +182 -0
- package/src/Listbox/dictionary.js +8 -0
- package/src/Shortcuts/Shortcuts.jsx +174 -0
- package/src/Shortcuts/ShortcutsItem.jsx +297 -0
- package/src/Shortcuts/index.js +4 -0
- package/src/Tooltip/Tooltip.native.jsx +2 -1
- package/src/index.js +1 -0
- package/types/Listbox.d.ts +24 -0
- package/types/Shortcuts.d.ts +136 -0
- package/types/index.d.ts +12 -0
|
@@ -2,21 +2,36 @@
|
|
|
2
2
|
import React from 'react'
|
|
3
3
|
import PropTypes from 'prop-types'
|
|
4
4
|
import { View, StyleSheet, Platform } from 'react-native'
|
|
5
|
-
import { withLinkRouter } from '../utils'
|
|
5
|
+
import { withLinkRouter, variantProp, copyPropTypes } from '../utils'
|
|
6
|
+
import { useThemeTokens } from '../ThemeProvider'
|
|
6
7
|
import ExpandCollapse from '../ExpandCollapse'
|
|
7
8
|
import ListboxItem from './ListboxItem'
|
|
8
9
|
import { useListboxContext } from './ListboxContext'
|
|
9
10
|
import GroupControl from './GroupControl'
|
|
11
|
+
import SecondLevelHeader from './SecondLevelHeader'
|
|
12
|
+
import defaultDictionary from './dictionary'
|
|
10
13
|
|
|
11
14
|
const styles = StyleSheet.create({
|
|
12
15
|
groupWrapper: {
|
|
13
16
|
margin: 0,
|
|
14
17
|
padding: 0,
|
|
15
|
-
overflow: '
|
|
18
|
+
overflow: 'visible'
|
|
16
19
|
},
|
|
17
20
|
list: {
|
|
18
21
|
margin: 0,
|
|
19
22
|
padding: 0
|
|
23
|
+
},
|
|
24
|
+
secondLevelContainer: {
|
|
25
|
+
margin: 0,
|
|
26
|
+
padding: 0,
|
|
27
|
+
width: '100%',
|
|
28
|
+
display: 'flex',
|
|
29
|
+
flexDirection: 'column'
|
|
30
|
+
},
|
|
31
|
+
secondLevelList: {
|
|
32
|
+
margin: 0,
|
|
33
|
+
padding: 0,
|
|
34
|
+
width: '100%'
|
|
20
35
|
}
|
|
21
36
|
})
|
|
22
37
|
|
|
@@ -27,6 +42,10 @@ const getAccessibilityRole = () =>
|
|
|
27
42
|
web: 'listitem'
|
|
28
43
|
})
|
|
29
44
|
|
|
45
|
+
const selectSecondLevelContainerStyles = ({ secondLevelHeaderBackgroundColor }) => ({
|
|
46
|
+
backgroundColor: secondLevelHeaderBackgroundColor
|
|
47
|
+
})
|
|
48
|
+
|
|
30
49
|
const ListboxGroup = React.forwardRef(
|
|
31
50
|
(
|
|
32
51
|
{
|
|
@@ -38,11 +57,86 @@ const ListboxGroup = React.forwardRef(
|
|
|
38
57
|
expandProps,
|
|
39
58
|
onLastItemBlur,
|
|
40
59
|
nextItemRef,
|
|
41
|
-
prevItemRef
|
|
60
|
+
prevItemRef,
|
|
61
|
+
copy = 'en',
|
|
62
|
+
dictionary = defaultDictionary,
|
|
63
|
+
variant = {},
|
|
64
|
+
tokens = {},
|
|
65
|
+
onClose
|
|
42
66
|
},
|
|
43
67
|
ref
|
|
44
68
|
) => {
|
|
45
|
-
const { selectedId } = useListboxContext()
|
|
69
|
+
const { selectedId, activeSecondLevelGroup, setActiveSecondLevelGroup } = useListboxContext()
|
|
70
|
+
const [secondLevelOpen, setSecondLevelOpen] = React.useState(false)
|
|
71
|
+
const isSecondLevel = variant?.secondLevel === true
|
|
72
|
+
const listboxTokens = useThemeTokens('Listbox', variant, tokens)
|
|
73
|
+
const groupId = id ?? label
|
|
74
|
+
|
|
75
|
+
const handleGroupClick = React.useCallback(() => {
|
|
76
|
+
if (isSecondLevel) {
|
|
77
|
+
setSecondLevelOpen(true)
|
|
78
|
+
setActiveSecondLevelGroup(groupId)
|
|
79
|
+
}
|
|
80
|
+
}, [isSecondLevel, groupId, setActiveSecondLevelGroup])
|
|
81
|
+
|
|
82
|
+
const handleBackClick = React.useCallback(() => {
|
|
83
|
+
setSecondLevelOpen(false)
|
|
84
|
+
setActiveSecondLevelGroup(null)
|
|
85
|
+
}, [setActiveSecondLevelGroup])
|
|
86
|
+
|
|
87
|
+
const handleCloseClick = React.useCallback(() => {
|
|
88
|
+
setSecondLevelOpen(false)
|
|
89
|
+
setActiveSecondLevelGroup(null)
|
|
90
|
+
if (onClose) {
|
|
91
|
+
onClose()
|
|
92
|
+
}
|
|
93
|
+
}, [setActiveSecondLevelGroup, onClose])
|
|
94
|
+
|
|
95
|
+
if (isSecondLevel && activeSecondLevelGroup && activeSecondLevelGroup !== groupId) {
|
|
96
|
+
return null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (isSecondLevel && secondLevelOpen) {
|
|
100
|
+
return (
|
|
101
|
+
<View
|
|
102
|
+
style={[styles.secondLevelContainer, selectSecondLevelContainerStyles(listboxTokens)]}
|
|
103
|
+
>
|
|
104
|
+
<SecondLevelHeader
|
|
105
|
+
label={label}
|
|
106
|
+
onBack={handleBackClick}
|
|
107
|
+
onClose={handleCloseClick}
|
|
108
|
+
copy={copy}
|
|
109
|
+
dictionary={dictionary}
|
|
110
|
+
variant={variant}
|
|
111
|
+
tokens={tokens}
|
|
112
|
+
/>
|
|
113
|
+
<View style={styles.secondLevelList}>
|
|
114
|
+
{items &&
|
|
115
|
+
items.map((item, index) => {
|
|
116
|
+
return (
|
|
117
|
+
<ListboxItem
|
|
118
|
+
key={item.label}
|
|
119
|
+
id={item.id ?? item.label}
|
|
120
|
+
{...item}
|
|
121
|
+
selected={
|
|
122
|
+
(item.id && item.id === selectedId) ||
|
|
123
|
+
(item.label && item.label === selectedId)
|
|
124
|
+
}
|
|
125
|
+
isChild={false}
|
|
126
|
+
LinkRouter={LinkRouter}
|
|
127
|
+
linkRouterProps={linkRouterProps}
|
|
128
|
+
variant={variant}
|
|
129
|
+
tokens={tokens}
|
|
130
|
+
{...(index === 0 && { prevItemRef })}
|
|
131
|
+
{...(index === items.length - 1 && { nextItemRef })}
|
|
132
|
+
{...(index === items.length - 1 && { onBlur: onLastItemBlur })}
|
|
133
|
+
/>
|
|
134
|
+
)
|
|
135
|
+
})}
|
|
136
|
+
</View>
|
|
137
|
+
</View>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
46
140
|
|
|
47
141
|
// TODO: implement keyboard navigation via refs for grouped items separately here
|
|
48
142
|
return (
|
|
@@ -63,9 +157,11 @@ const ListboxGroup = React.forwardRef(
|
|
|
63
157
|
// TODO refactor
|
|
64
158
|
// eslint-disable-next-line react/no-unstable-nested-components
|
|
65
159
|
control={(controlProps) => (
|
|
66
|
-
<GroupControl id={id ?? label} {...controlProps} label={label} />
|
|
160
|
+
<GroupControl id={id ?? label} {...controlProps} label={label} variant={variant} />
|
|
67
161
|
)}
|
|
68
162
|
{...expandProps}
|
|
163
|
+
{...(isSecondLevel && { open: false })}
|
|
164
|
+
{...(isSecondLevel && { onPress: handleGroupClick })}
|
|
69
165
|
tokens={{
|
|
70
166
|
contentPaddingLeft: 0,
|
|
71
167
|
contentPaddingRight: 0,
|
|
@@ -79,26 +175,31 @@ const ListboxGroup = React.forwardRef(
|
|
|
79
175
|
}}
|
|
80
176
|
controlRef={ref}
|
|
81
177
|
>
|
|
82
|
-
|
|
83
|
-
{
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
178
|
+
{!isSecondLevel && (
|
|
179
|
+
<View style={styles.list}>
|
|
180
|
+
{items.map((item, index) => {
|
|
181
|
+
return (
|
|
182
|
+
<ListboxItem
|
|
183
|
+
key={item.label}
|
|
184
|
+
id={item.id ?? item.label}
|
|
185
|
+
{...item}
|
|
186
|
+
selected={
|
|
187
|
+
(item.id && item.id === selectedId) ||
|
|
188
|
+
(item.label && item.label === selectedId)
|
|
189
|
+
}
|
|
190
|
+
isChild
|
|
191
|
+
LinkRouter={LinkRouter}
|
|
192
|
+
linkRouterProps={linkRouterProps}
|
|
193
|
+
variant={variant}
|
|
194
|
+
tokens={tokens}
|
|
195
|
+
{...(index === 0 && { prevItemRef })}
|
|
196
|
+
{...(index === items.length - 1 && { nextItemRef })}
|
|
197
|
+
{...(index === items.length - 1 && { onBlur: onLastItemBlur })}
|
|
198
|
+
/>
|
|
199
|
+
)
|
|
200
|
+
})}
|
|
201
|
+
</View>
|
|
202
|
+
)}
|
|
102
203
|
</ExpandCollapse.Panel>
|
|
103
204
|
</View>
|
|
104
205
|
)
|
|
@@ -108,6 +209,10 @@ ListboxGroup.displayName = 'ListboxGroup'
|
|
|
108
209
|
|
|
109
210
|
ListboxGroup.propTypes = {
|
|
110
211
|
...withLinkRouter.propTypes,
|
|
212
|
+
/**
|
|
213
|
+
* Unique identifier for the group
|
|
214
|
+
*/
|
|
215
|
+
id: PropTypes.string,
|
|
111
216
|
label: PropTypes.string,
|
|
112
217
|
items: PropTypes.arrayOf(
|
|
113
218
|
PropTypes.shape({
|
|
@@ -122,7 +227,34 @@ ListboxGroup.propTypes = {
|
|
|
122
227
|
/**
|
|
123
228
|
* Use this callback to redirect the focus after it leaves the last item of the group.
|
|
124
229
|
*/
|
|
125
|
-
onLastItemBlur: PropTypes.func
|
|
230
|
+
onLastItemBlur: PropTypes.func,
|
|
231
|
+
/**
|
|
232
|
+
* Select English or French copy
|
|
233
|
+
*/
|
|
234
|
+
copy: copyPropTypes,
|
|
235
|
+
/**
|
|
236
|
+
* Override the default dictionary, by passing the complete dictionary object for `en` and `fr`
|
|
237
|
+
*/
|
|
238
|
+
dictionary: PropTypes.shape({
|
|
239
|
+
en: PropTypes.shape({
|
|
240
|
+
closeMenu: PropTypes.string.isRequired
|
|
241
|
+
}),
|
|
242
|
+
fr: PropTypes.shape({
|
|
243
|
+
closeMenu: PropTypes.string.isRequired
|
|
244
|
+
})
|
|
245
|
+
}),
|
|
246
|
+
/**
|
|
247
|
+
* Variant configuration for secondLevel behavior
|
|
248
|
+
*/
|
|
249
|
+
variant: variantProp.propType,
|
|
250
|
+
/**
|
|
251
|
+
* Custom tokens
|
|
252
|
+
*/
|
|
253
|
+
tokens: PropTypes.object,
|
|
254
|
+
/**
|
|
255
|
+
* Callback when the menu is closed
|
|
256
|
+
*/
|
|
257
|
+
onClose: PropTypes.func
|
|
126
258
|
}
|
|
127
259
|
|
|
128
260
|
export default ListboxGroup
|
|
@@ -23,10 +23,21 @@ const paddingHorizontal = 0
|
|
|
23
23
|
|
|
24
24
|
const DropdownOverlay = React.forwardRef(
|
|
25
25
|
(
|
|
26
|
-
{
|
|
26
|
+
{
|
|
27
|
+
children,
|
|
28
|
+
isReady = false,
|
|
29
|
+
overlaidPosition,
|
|
30
|
+
maxWidth,
|
|
31
|
+
minWidth,
|
|
32
|
+
onLayout,
|
|
33
|
+
tokens,
|
|
34
|
+
testID,
|
|
35
|
+
variant
|
|
36
|
+
},
|
|
37
|
+
|
|
27
38
|
ref
|
|
28
39
|
) => {
|
|
29
|
-
const systemTokens = useThemeTokens('Listbox',
|
|
40
|
+
const systemTokens = useThemeTokens('Listbox', variant, tokens)
|
|
30
41
|
|
|
31
42
|
return (
|
|
32
43
|
<View
|
|
@@ -43,11 +54,12 @@ const DropdownOverlay = React.forwardRef(
|
|
|
43
54
|
<Card
|
|
44
55
|
tokens={{
|
|
45
56
|
shadow: systemTokens.shadow,
|
|
57
|
+
borderRadius: systemTokens.borderRadius,
|
|
58
|
+
...(Platform.OS === 'web' && { overflowY: 'hidden' }),
|
|
46
59
|
paddingBottom: paddingVertical,
|
|
47
60
|
paddingTop: paddingVertical,
|
|
48
61
|
paddingLeft: paddingHorizontal,
|
|
49
|
-
paddingRight: paddingHorizontal
|
|
50
|
-
...tokens
|
|
62
|
+
paddingRight: paddingHorizontal
|
|
51
63
|
}}
|
|
52
64
|
>
|
|
53
65
|
{children}
|
|
@@ -80,7 +92,8 @@ DropdownOverlay.propTypes = {
|
|
|
80
92
|
minWidth: PropTypes.number,
|
|
81
93
|
onLayout: PropTypes.func,
|
|
82
94
|
tokens: PropTypes.object,
|
|
83
|
-
testID: PropTypes.string
|
|
95
|
+
testID: PropTypes.string,
|
|
96
|
+
variant: PropTypes.object
|
|
84
97
|
}
|
|
85
98
|
|
|
86
99
|
export default Platform.OS === 'web' ? withPortal(DropdownOverlay) : DropdownOverlay
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import { View, StyleSheet, Text, Pressable } from 'react-native'
|
|
4
|
+
import { useThemeTokens } from '../ThemeProvider'
|
|
5
|
+
import { useCopy, variantProp, copyPropTypes } from '../utils'
|
|
6
|
+
import Icon from '../Icon'
|
|
7
|
+
import IconButton from '../IconButton'
|
|
8
|
+
import Divider from '../Divider'
|
|
9
|
+
import defaultDictionary from './dictionary'
|
|
10
|
+
|
|
11
|
+
const styles = StyleSheet.create({
|
|
12
|
+
headerContainer: {
|
|
13
|
+
width: '100%'
|
|
14
|
+
},
|
|
15
|
+
headerContent: {
|
|
16
|
+
flexDirection: 'row',
|
|
17
|
+
alignItems: 'center',
|
|
18
|
+
width: '100%'
|
|
19
|
+
},
|
|
20
|
+
leftSection: {
|
|
21
|
+
flexDirection: 'row',
|
|
22
|
+
alignItems: 'center',
|
|
23
|
+
flex: 1
|
|
24
|
+
},
|
|
25
|
+
backIcon: {
|
|
26
|
+
marginRight: 8,
|
|
27
|
+
flexShrink: 0
|
|
28
|
+
},
|
|
29
|
+
labelText: {
|
|
30
|
+
flex: 1
|
|
31
|
+
},
|
|
32
|
+
closeButton: {
|
|
33
|
+
flexShrink: 0
|
|
34
|
+
},
|
|
35
|
+
dividerContainer: {
|
|
36
|
+
width: '100%'
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const selectHeaderContainerStyles = ({ secondLevelHeaderBackgroundColor }) => ({
|
|
41
|
+
backgroundColor: secondLevelHeaderBackgroundColor
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const selectHeaderContentStyles = ({
|
|
45
|
+
secondLevelHeaderPaddingTop,
|
|
46
|
+
secondLevelHeaderPaddingBottom,
|
|
47
|
+
secondLevelHeaderPaddingLeft,
|
|
48
|
+
secondLevelHeaderPaddingRight
|
|
49
|
+
}) => ({
|
|
50
|
+
paddingTop: secondLevelHeaderPaddingTop,
|
|
51
|
+
paddingBottom: secondLevelHeaderPaddingBottom,
|
|
52
|
+
paddingLeft: secondLevelHeaderPaddingLeft,
|
|
53
|
+
paddingRight: secondLevelHeaderPaddingRight
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const selectLabelTextStyles = ({
|
|
57
|
+
secondLevelBackLinkFontName,
|
|
58
|
+
secondLevelBackLinkFontWeight,
|
|
59
|
+
secondLevelBackLinkFontSize,
|
|
60
|
+
secondLevelBackLinkColor
|
|
61
|
+
}) => ({
|
|
62
|
+
fontFamily: `${secondLevelBackLinkFontName}${secondLevelBackLinkFontWeight}normal`,
|
|
63
|
+
fontSize: secondLevelBackLinkFontSize,
|
|
64
|
+
color: secondLevelBackLinkColor
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* SecondLevelHeader component for Listbox secondLevel variant.
|
|
69
|
+
* Displays a header with back button icon, title text, and close button (IconButton),
|
|
70
|
+
* separated from content by a Divider.
|
|
71
|
+
*/
|
|
72
|
+
const SecondLevelHeader = React.forwardRef(
|
|
73
|
+
(
|
|
74
|
+
{
|
|
75
|
+
label,
|
|
76
|
+
onBack,
|
|
77
|
+
onClose,
|
|
78
|
+
copy = 'en',
|
|
79
|
+
dictionary = defaultDictionary,
|
|
80
|
+
tokens: tokensProp = {},
|
|
81
|
+
variant = {}
|
|
82
|
+
},
|
|
83
|
+
ref
|
|
84
|
+
) => {
|
|
85
|
+
const tokens = useThemeTokens('Listbox', variant, tokensProp)
|
|
86
|
+
const getCopy = useCopy({ dictionary, copy })
|
|
87
|
+
|
|
88
|
+
const {
|
|
89
|
+
secondLevelBackIcon,
|
|
90
|
+
secondLevelBackIconColor,
|
|
91
|
+
secondLevelCloseIcon,
|
|
92
|
+
secondLevelCloseIconSize,
|
|
93
|
+
secondLevelCloseButtonBorderWidth,
|
|
94
|
+
secondLevelCloseButtonPadding,
|
|
95
|
+
secondLevelDividerColor,
|
|
96
|
+
secondLevelDividerWidth
|
|
97
|
+
} = tokens
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<View style={[styles.headerContainer, selectHeaderContainerStyles(tokens)]} ref={ref}>
|
|
101
|
+
<View style={[styles.headerContent, selectHeaderContentStyles(tokens)]}>
|
|
102
|
+
<Pressable onPress={onBack} style={styles.leftSection}>
|
|
103
|
+
<View style={styles.backIcon}>
|
|
104
|
+
<Icon
|
|
105
|
+
icon={secondLevelBackIcon}
|
|
106
|
+
tokens={{
|
|
107
|
+
color: secondLevelBackIconColor
|
|
108
|
+
}}
|
|
109
|
+
variant={{ size: 'micro' }}
|
|
110
|
+
/>
|
|
111
|
+
</View>
|
|
112
|
+
<Text numberOfLines={1} style={[styles.labelText, selectLabelTextStyles(tokens)]}>
|
|
113
|
+
{label}
|
|
114
|
+
</Text>
|
|
115
|
+
</Pressable>
|
|
116
|
+
<View style={styles.closeButton}>
|
|
117
|
+
<IconButton
|
|
118
|
+
icon={secondLevelCloseIcon}
|
|
119
|
+
onPress={onClose}
|
|
120
|
+
accessibilityLabel={getCopy('closeMenu')}
|
|
121
|
+
tokens={{
|
|
122
|
+
iconSize: secondLevelCloseIconSize,
|
|
123
|
+
borderWidth: secondLevelCloseButtonBorderWidth,
|
|
124
|
+
padding: secondLevelCloseButtonPadding
|
|
125
|
+
}}
|
|
126
|
+
/>
|
|
127
|
+
</View>
|
|
128
|
+
</View>
|
|
129
|
+
<View style={styles.dividerContainer}>
|
|
130
|
+
<Divider
|
|
131
|
+
tokens={{
|
|
132
|
+
color: secondLevelDividerColor,
|
|
133
|
+
width: secondLevelDividerWidth
|
|
134
|
+
}}
|
|
135
|
+
/>
|
|
136
|
+
</View>
|
|
137
|
+
</View>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
SecondLevelHeader.displayName = 'SecondLevelHeader'
|
|
143
|
+
|
|
144
|
+
SecondLevelHeader.propTypes = {
|
|
145
|
+
/**
|
|
146
|
+
* The label text to display (typically the parent item label)
|
|
147
|
+
*/
|
|
148
|
+
label: PropTypes.string.isRequired,
|
|
149
|
+
/**
|
|
150
|
+
* Callback when back button is clicked
|
|
151
|
+
*/
|
|
152
|
+
onBack: PropTypes.func.isRequired,
|
|
153
|
+
/**
|
|
154
|
+
* Callback when close button is clicked
|
|
155
|
+
*/
|
|
156
|
+
onClose: PropTypes.func.isRequired,
|
|
157
|
+
/**
|
|
158
|
+
* Select English or French copy
|
|
159
|
+
*/
|
|
160
|
+
copy: copyPropTypes,
|
|
161
|
+
/**
|
|
162
|
+
* Override the default dictionary, by passing the complete dictionary object for `en` and `fr`
|
|
163
|
+
*/
|
|
164
|
+
dictionary: PropTypes.shape({
|
|
165
|
+
en: PropTypes.shape({
|
|
166
|
+
closeMenu: PropTypes.string.isRequired
|
|
167
|
+
}),
|
|
168
|
+
fr: PropTypes.shape({
|
|
169
|
+
closeMenu: PropTypes.string.isRequired
|
|
170
|
+
})
|
|
171
|
+
}),
|
|
172
|
+
/**
|
|
173
|
+
* Custom tokens to override theme tokens
|
|
174
|
+
*/
|
|
175
|
+
tokens: PropTypes.object,
|
|
176
|
+
/**
|
|
177
|
+
* Variant configuration
|
|
178
|
+
*/
|
|
179
|
+
variant: variantProp.propType
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export default SecondLevelHeader
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import { Platform, StyleSheet, View } from 'react-native'
|
|
4
|
+
import { viewports } from '@telus-uds/system-constants'
|
|
5
|
+
|
|
6
|
+
import { useTheme, useThemeTokens } from '../ThemeProvider'
|
|
7
|
+
import { useViewport } from '../ViewportProvider'
|
|
8
|
+
import {
|
|
9
|
+
a11yProps,
|
|
10
|
+
getTokensPropType,
|
|
11
|
+
selectSystemProps,
|
|
12
|
+
useResponsiveProp,
|
|
13
|
+
variantProp,
|
|
14
|
+
viewProps
|
|
15
|
+
} from '../utils'
|
|
16
|
+
|
|
17
|
+
import HorizontalScroll, {
|
|
18
|
+
horizontalScrollUtils,
|
|
19
|
+
HorizontalScrollButton
|
|
20
|
+
} from '../HorizontalScroll'
|
|
21
|
+
|
|
22
|
+
const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
|
|
23
|
+
|
|
24
|
+
const { selectHorizontalScrollTokens, useItemPositions } = horizontalScrollUtils
|
|
25
|
+
|
|
26
|
+
const selectStyles = (themeTokens, maxWidth, viewport) => {
|
|
27
|
+
const isDesktop =
|
|
28
|
+
viewport === viewports.md || viewport === viewports.lg || viewport === viewports.xl
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
wrapper: {
|
|
32
|
+
alignItems: isDesktop ? 'center' : 'flex-start'
|
|
33
|
+
},
|
|
34
|
+
scrollContainer: {
|
|
35
|
+
width: '100%',
|
|
36
|
+
...(isDesktop && { maxWidth })
|
|
37
|
+
},
|
|
38
|
+
container: {
|
|
39
|
+
paddingTop: themeTokens.mainContainerTopPadding,
|
|
40
|
+
paddingBottom: themeTokens.mainContainerBottomPadding,
|
|
41
|
+
paddingLeft: themeTokens.mainContainerLeftPadding,
|
|
42
|
+
paddingRight: themeTokens.mainContainerRightPadding,
|
|
43
|
+
gap: themeTokens.mainContainerGap,
|
|
44
|
+
...(isDesktop && {
|
|
45
|
+
alignItems: 'flex-start',
|
|
46
|
+
justifyContent: 'center'
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A horizontal scrollable shortcuts component that displays a collection of shortcut items.
|
|
54
|
+
* This component automatically injects shared configuration props to all ShortcutsItem children
|
|
55
|
+
* via React.cloneElement, including variant settings, hideLabels, and iconVariant.
|
|
56
|
+
*
|
|
57
|
+
* @component
|
|
58
|
+
* @param {Object} props - Component properties
|
|
59
|
+
* @param {Object} [props.tokens] - Theme tokens to customize the component's appearance
|
|
60
|
+
* @param {Object} [props.variant] - Visual variant configuration for the shortcuts container and its items
|
|
61
|
+
* @param {string} [props.variant.width] - Width variant to apply to all items (e.g., 'equal', 'dynamic')
|
|
62
|
+
* @param {Object} [props.scrollButtonTokens] - Tokens to customize scroll button appearance
|
|
63
|
+
* @param {boolean} [props.hideLabels=false] - Whether to hide labels on all shortcut items (can be overridden per item)
|
|
64
|
+
* @param {Object} [props.iconVariant] - Icon variant to apply to all shortcut items (can be overridden per item)
|
|
65
|
+
* @param {React.ReactNode} props.children - ShortcutsItem components to render
|
|
66
|
+
* @param {React.Ref} ref - Forwarded ref to the component's root element
|
|
67
|
+
* @returns {React.ReactElement} Rendered shortcuts component with horizontal scroll functionality
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* <Shortcuts hideLabels={false} variant={{ width: 'equal' }}>
|
|
71
|
+
* <ShortcutsItem icon={HomeIcon} label="Home" href="/home" />
|
|
72
|
+
* <ShortcutsItem icon={SettingsIcon} label="Settings" href="/settings" />
|
|
73
|
+
* </Shortcuts>
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* // Item-level props override container props
|
|
77
|
+
* <Shortcuts hideLabels iconVariant={{ size: 'small' }}>
|
|
78
|
+
* <ShortcutsItem icon={HomeIcon} label="Home" hideLabel={false} />
|
|
79
|
+
* <ShortcutsItem icon={SettingsIcon} label="Settings" />
|
|
80
|
+
* </Shortcuts>
|
|
81
|
+
*/
|
|
82
|
+
const Shortcuts = React.forwardRef(
|
|
83
|
+
(
|
|
84
|
+
{ tokens, variant, scrollButtonTokens, hideLabels = false, iconVariant, children, ...rest },
|
|
85
|
+
ref
|
|
86
|
+
) => {
|
|
87
|
+
const viewport = useViewport()
|
|
88
|
+
const themeTokens = useThemeTokens('Shortcuts', tokens, variant, {
|
|
89
|
+
viewport
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const { themeOptions } = useTheme()
|
|
93
|
+
const maxWidth = useResponsiveProp(
|
|
94
|
+
themeOptions?.contentMaxWidth,
|
|
95
|
+
viewports.map.get(viewports.xl)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
const [itemPositions] = useItemPositions()
|
|
99
|
+
|
|
100
|
+
const [maxItemWidth, setMaxItemWidth] = React.useState(null)
|
|
101
|
+
|
|
102
|
+
const registerWidth = React.useCallback(
|
|
103
|
+
(width) => setMaxItemWidth((prev) => (prev == null || width > prev ? width : prev)),
|
|
104
|
+
[]
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
const styles = selectStyles(themeTokens, maxWidth, viewport)
|
|
108
|
+
|
|
109
|
+
const childrenWithProps = React.Children.map(children, (child) => {
|
|
110
|
+
if (!React.isValidElement(child)) {
|
|
111
|
+
return child
|
|
112
|
+
}
|
|
113
|
+
return React.cloneElement(child, {
|
|
114
|
+
maxWidth: maxItemWidth,
|
|
115
|
+
registerWidth,
|
|
116
|
+
containerVariant: variant,
|
|
117
|
+
containerHideLabels: hideLabels,
|
|
118
|
+
containerIconVariant: iconVariant
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<View style={[staticStyles.wrapper, styles.wrapper]} ref={ref} {...selectProps(rest)}>
|
|
124
|
+
<View style={styles.scrollContainer}>
|
|
125
|
+
<HorizontalScroll
|
|
126
|
+
ScrollButton={HorizontalScrollButton}
|
|
127
|
+
itemPositions={itemPositions}
|
|
128
|
+
tokens={selectHorizontalScrollTokens(themeTokens)}
|
|
129
|
+
scrollButtonTokens={scrollButtonTokens}
|
|
130
|
+
variant={{ hideNavigationButtons: Platform.OS !== 'web' }}
|
|
131
|
+
>
|
|
132
|
+
<View style={[staticStyles.container, styles.container]}>{childrenWithProps}</View>
|
|
133
|
+
</HorizontalScroll>
|
|
134
|
+
</View>
|
|
135
|
+
</View>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
Shortcuts.displayName = 'Shortcuts'
|
|
141
|
+
|
|
142
|
+
Shortcuts.propTypes = {
|
|
143
|
+
...selectedSystemPropTypes,
|
|
144
|
+
tokens: getTokensPropType('Shortcuts'),
|
|
145
|
+
variant: variantProp.propType,
|
|
146
|
+
/**
|
|
147
|
+
* Custom tokens for `HorizontalScrollButton`
|
|
148
|
+
*/
|
|
149
|
+
scrollButtonTokens: getTokensPropType('HorizontalScrollButton'),
|
|
150
|
+
/**
|
|
151
|
+
* Hide labels for all ShortcutsItem children. When true, labels are visually hidden but remain accessible to screen readers via the icon's accessibilityLabel.
|
|
152
|
+
*/
|
|
153
|
+
hideLabels: PropTypes.bool,
|
|
154
|
+
/**
|
|
155
|
+
* Icon variant to apply to all ShortcutsItem children.
|
|
156
|
+
*/
|
|
157
|
+
iconVariant: variantProp.propType,
|
|
158
|
+
/**
|
|
159
|
+
* ShortcutsItem components to be rendered within the Shortcuts container
|
|
160
|
+
*/
|
|
161
|
+
children: PropTypes.node
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const staticStyles = StyleSheet.create({
|
|
165
|
+
wrapper: {
|
|
166
|
+
flexGrow: 1
|
|
167
|
+
},
|
|
168
|
+
container: {
|
|
169
|
+
flexDirection: 'row',
|
|
170
|
+
flex: 1
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
export default Shortcuts
|