@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.
Files changed (51) hide show
  1. package/CHANGELOG.md +12 -1
  2. package/lib/cjs/Card/CardBase.js +97 -17
  3. package/lib/cjs/Card/PressableCardBase.js +12 -8
  4. package/lib/cjs/HorizontalScroll/HorizontalScroll.js +5 -2
  5. package/lib/cjs/Icon/Icon.js +3 -0
  6. package/lib/cjs/Listbox/GroupControl.js +12 -6
  7. package/lib/cjs/Listbox/Listbox.js +41 -7
  8. package/lib/cjs/Listbox/ListboxGroup.js +139 -8
  9. package/lib/cjs/Listbox/ListboxOverlay.js +10 -5
  10. package/lib/cjs/Listbox/SecondLevelHeader.js +201 -0
  11. package/lib/cjs/Listbox/dictionary.js +14 -0
  12. package/lib/cjs/Shortcuts/Shortcuts.js +169 -0
  13. package/lib/cjs/Shortcuts/ShortcutsItem.js +280 -0
  14. package/lib/cjs/Shortcuts/index.js +16 -0
  15. package/lib/cjs/Tooltip/Tooltip.native.js +2 -0
  16. package/lib/cjs/index.js +15 -0
  17. package/lib/esm/Card/CardBase.js +97 -17
  18. package/lib/esm/Card/PressableCardBase.js +10 -8
  19. package/lib/esm/HorizontalScroll/HorizontalScroll.js +6 -3
  20. package/lib/esm/Icon/Icon.js +3 -0
  21. package/lib/esm/Listbox/GroupControl.js +12 -6
  22. package/lib/esm/Listbox/Listbox.js +41 -7
  23. package/lib/esm/Listbox/ListboxGroup.js +141 -10
  24. package/lib/esm/Listbox/ListboxOverlay.js +10 -5
  25. package/lib/esm/Listbox/SecondLevelHeader.js +194 -0
  26. package/lib/esm/Listbox/dictionary.js +8 -0
  27. package/lib/esm/Shortcuts/Shortcuts.js +160 -0
  28. package/lib/esm/Shortcuts/ShortcutsItem.js +273 -0
  29. package/lib/esm/Shortcuts/index.js +3 -0
  30. package/lib/esm/Tooltip/Tooltip.native.js +2 -0
  31. package/lib/esm/index.js +1 -0
  32. package/lib/package.json +2 -2
  33. package/package.json +2 -2
  34. package/src/Card/CardBase.jsx +113 -14
  35. package/src/Card/PressableCardBase.jsx +17 -5
  36. package/src/HorizontalScroll/HorizontalScroll.jsx +6 -3
  37. package/src/Icon/Icon.jsx +3 -0
  38. package/src/Listbox/GroupControl.jsx +41 -33
  39. package/src/Listbox/Listbox.jsx +41 -2
  40. package/src/Listbox/ListboxGroup.jsx +158 -26
  41. package/src/Listbox/ListboxOverlay.jsx +18 -5
  42. package/src/Listbox/SecondLevelHeader.jsx +182 -0
  43. package/src/Listbox/dictionary.js +8 -0
  44. package/src/Shortcuts/Shortcuts.jsx +174 -0
  45. package/src/Shortcuts/ShortcutsItem.jsx +297 -0
  46. package/src/Shortcuts/index.js +4 -0
  47. package/src/Tooltip/Tooltip.native.jsx +2 -1
  48. package/src/index.js +1 -0
  49. package/types/Listbox.d.ts +24 -0
  50. package/types/Shortcuts.d.ts +136 -0
  51. package/types/index.d.ts +12 -0
@@ -6,9 +6,18 @@ import { applyShadowToken } from '../ThemeProvider'
6
6
  import { getTokensPropType, responsiveProps, useResponsiveProp, formatImageSource } from '../utils'
7
7
  import { a11yProps, viewProps, selectSystemProps } from '../utils/props'
8
8
  import backgroundImageStylesMap from './backgroundImageStylesMap'
9
+ import FlexGrid from '../FlexGrid/FlexGrid'
10
+ import FlexGridRow from '../FlexGrid/Row'
11
+ import FlexGridCol from '../FlexGrid/Col'
9
12
 
10
13
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
11
14
 
15
+ const GRID_COLUMNS = 12
16
+
17
+ const isOverlayColor = (color) => {
18
+ return color && typeof color === 'string' && color.startsWith('rgba(')
19
+ }
20
+
12
21
  const setBackgroundImage = ({
13
22
  src,
14
23
  alt,
@@ -22,12 +31,10 @@ const setBackgroundImage = ({
22
31
  const borderWidth = cardStyle?.borderWidth || 0
23
32
  const adjustedBorderRadius = Math.max(0, borderRadius - borderWidth)
24
33
 
25
- // For contain mode with position and align, use CSS background properties for web
26
34
  if (backgroundImageResizeMode === 'contain' && backgroundImagePosition && backgroundImageAlign) {
27
35
  const positionKey = `${backgroundImagePosition}-${backgroundImageAlign}`
28
36
 
29
37
  if (Platform.OS === 'web') {
30
- // Create background position based on position and align
31
38
  let backgroundPosition
32
39
 
33
40
  switch (positionKey) {
@@ -77,7 +84,7 @@ const setBackgroundImage = ({
77
84
  </View>
78
85
  )
79
86
  }
80
- // For React Native, apply positioning styles with full dimensions
87
+
81
88
  const positionStyles = backgroundImageStylesMap[positionKey] || {}
82
89
 
83
90
  return (
@@ -95,7 +102,6 @@ const setBackgroundImage = ({
95
102
  )
96
103
  }
97
104
 
98
- // Use ImageBackground for all other resize modes and React Native
99
105
  return (
100
106
  <ImageBackground
101
107
  source={src}
@@ -121,6 +127,10 @@ export const selectStyles = ({
121
127
  paddingLeft,
122
128
  paddingRight,
123
129
  paddingTop,
130
+ marginTop,
131
+ marginBottom,
132
+ marginLeft,
133
+ marginRight,
124
134
  minWidth,
125
135
  shadow,
126
136
  backgroundGradient,
@@ -128,9 +138,27 @@ export const selectStyles = ({
128
138
  maxHeight,
129
139
  overflowY
130
140
  }) => {
141
+ const hasGradient = (gradient || backgroundGradient) && Platform.OS === 'web'
142
+
143
+ let backgroundImageValue = null
144
+
145
+ if (hasGradient) {
146
+ const gradientObj = gradient || backgroundGradient
147
+ const gradientString = `linear-gradient(${gradientObj.angle}deg, ${gradientObj.stops[0].color}, ${gradientObj.stops[1].color})`
148
+ const shouldApplyOverlay =
149
+ (gradient || (backgroundGradient && backgroundColor && backgroundColor !== 'transparent')) &&
150
+ isOverlayColor(backgroundColor)
151
+
152
+ backgroundImageValue = shouldApplyOverlay
153
+ ? `linear-gradient(${backgroundColor}, ${backgroundColor}), ${gradientString}`
154
+ : gradientString
155
+ }
156
+
157
+ const boxShadowColor = isOverlayColor(backgroundColor) ? backgroundColor : 'white'
158
+
131
159
  return {
132
160
  flex,
133
- backgroundColor,
161
+ backgroundColor: hasGradient ? 'transparent' : backgroundColor,
134
162
  borderColor,
135
163
  borderRadius,
136
164
  borderWidth,
@@ -138,19 +166,23 @@ export const selectStyles = ({
138
166
  paddingLeft,
139
167
  paddingRight,
140
168
  paddingTop,
169
+ marginTop,
170
+ marginBottom,
171
+ marginLeft,
172
+ marginRight,
141
173
  minWidth,
142
174
  ...applyShadowToken(shadow),
143
175
  ...(gradient && Platform.OS === 'web'
144
176
  ? {
145
- backgroundImage: `linear-gradient(${gradient.angle}deg, ${gradient.stops[0].color}, ${gradient.stops[1].color})`,
177
+ backgroundImage: backgroundImageValue,
146
178
  backgroundOrigin: `border-box`,
147
- boxShadow: `inset 0 1000px white`,
179
+ boxShadow: `inset 0 1000px ${boxShadowColor}`,
148
180
  border: `${borderWidth}px solid transparent`
149
181
  }
150
182
  : {}),
151
183
  ...(backgroundGradient && Platform.OS === 'web'
152
184
  ? {
153
- backgroundImage: `linear-gradient(${backgroundGradient.angle}deg, ${backgroundGradient.stops[0].color}, ${backgroundGradient.stops[1].color})`
185
+ backgroundImage: backgroundImageValue
154
186
  }
155
187
  : {}),
156
188
  ...(Platform.OS === 'web' ? { maxHeight, overflowY } : {})
@@ -162,8 +194,8 @@ export const selectStyles = ({
162
194
  * intended to be used in apps or sites directly: build themed components on top of this.
163
195
  */
164
196
  const CardBase = React.forwardRef(
165
- ({ children, tokens, dataSet, backgroundImage, ...rest }, ref) => {
166
- const cardStyle = selectStyles(typeof tokens === 'function' ? tokens() : tokens)
197
+ ({ children, tokens, dataSet, backgroundImage, fullBleedContent, cardState, ...rest }, ref) => {
198
+ const cardStyle = selectStyles(typeof tokens === 'function' ? tokens(cardState) : tokens)
167
199
  const props = selectProps(rest)
168
200
 
169
201
  let content = children
@@ -174,12 +206,16 @@ const CardBase = React.forwardRef(
174
206
  const backgroundImageAlign = useResponsiveProp(align)
175
207
  const imageSourceViewport = formatImageSource(useResponsiveProp(src))
176
208
 
209
+ const {
210
+ content: fullBleedImageContent,
211
+ position: fullBleedContentPosition,
212
+ imgCol
213
+ } = fullBleedContent || {}
214
+ const fullBleedPosition = useResponsiveProp(fullBleedContentPosition, 'bottom')
215
+
177
216
  if (backgroundImage && src) {
178
- // When there's a background image, separate the padding from the container style
179
- // so the image can fill the entire container without padding interference
180
217
  const { paddingTop, paddingBottom, paddingLeft, paddingRight, ...containerStyle } = cardStyle
181
218
 
182
- // Only create padding wrapper if there's actually padding defined
183
219
  const hasPadding = paddingTop || paddingBottom || paddingLeft || paddingRight
184
220
  const paddedContent = hasPadding ? (
185
221
  <View style={{ paddingTop, paddingBottom, paddingLeft, paddingRight }}>{children}</View>
@@ -204,6 +240,53 @@ const CardBase = React.forwardRef(
204
240
  )
205
241
  }
206
242
 
243
+ if (fullBleedContent && fullBleedImageContent) {
244
+ const { paddingTop, paddingBottom, paddingLeft, paddingRight, ...containerStyle } = cardStyle
245
+ const imageColumns = imgCol || { xs: GRID_COLUMNS }
246
+
247
+ const textColumns = {}
248
+ Object.keys(imageColumns).forEach((breakpoint) => {
249
+ textColumns[breakpoint] = GRID_COLUMNS - (imageColumns[breakpoint] || GRID_COLUMNS)
250
+ })
251
+
252
+ const imageFirst = fullBleedPosition === 'top' || fullBleedPosition === 'left'
253
+
254
+ const imageColContent = (
255
+ <FlexGridCol {...imageColumns} style={staticStyles.fullBleedImageCol}>
256
+ {fullBleedImageContent}
257
+ </FlexGridCol>
258
+ )
259
+
260
+ const textColContent = (
261
+ <FlexGridCol
262
+ {...textColumns}
263
+ style={{ paddingTop, paddingBottom, paddingLeft, paddingRight }}
264
+ >
265
+ {children}
266
+ </FlexGridCol>
267
+ )
268
+
269
+ return (
270
+ <View style={containerStyle} dataSet={dataSet} ref={ref} {...props}>
271
+ <FlexGrid>
272
+ <FlexGridRow>
273
+ {imageFirst ? (
274
+ <>
275
+ {imageColContent}
276
+ {textColContent}
277
+ </>
278
+ ) : (
279
+ <>
280
+ {textColContent}
281
+ {imageColContent}
282
+ </>
283
+ )}
284
+ </FlexGridRow>
285
+ </FlexGrid>
286
+ </View>
287
+ )
288
+ }
289
+
207
290
  return (
208
291
  <View style={cardStyle} dataSet={dataSet} ref={ref} {...props}>
209
292
  {content}
@@ -213,6 +296,18 @@ const CardBase = React.forwardRef(
213
296
  )
214
297
  CardBase.displayName = 'CardBase'
215
298
 
299
+ export const fullBleedContentPropTypes = PropTypes.shape({
300
+ content: PropTypes.node.isRequired,
301
+ alt: PropTypes.string,
302
+ position: responsiveProps.getTypeOptionallyByViewport(
303
+ PropTypes.oneOf(['bottom', 'left', 'right', 'top'])
304
+ ),
305
+ align: responsiveProps.getTypeOptionallyByViewport(
306
+ PropTypes.oneOf(['start', 'end', 'center', 'stretch'])
307
+ ),
308
+ imgCol: PropTypes.object
309
+ })
310
+
216
311
  const staticStyles = StyleSheet.create({
217
312
  imageBackground: { width: '100%', height: '100%' },
218
313
  contentOverlay: {
@@ -231,6 +326,9 @@ const staticStyles = StyleSheet.create({
231
326
  position: 'absolute',
232
327
  width: '100%',
233
328
  height: '100%'
329
+ },
330
+ fullBleedImageCol: {
331
+ padding: 0
234
332
  }
235
333
  })
236
334
 
@@ -255,7 +353,8 @@ CardBase.propTypes = {
255
353
  align: responsiveProps.getTypeOptionallyByViewport(
256
354
  PropTypes.oneOf(['start', 'end', 'center', 'stretch'])
257
355
  )
258
- })
356
+ }),
357
+ fullBleedContent: fullBleedContentPropTypes
259
358
  }
260
359
 
261
360
  export default CardBase
@@ -18,7 +18,7 @@ import {
18
18
  viewProps,
19
19
  withLinkRouter
20
20
  } from '../utils'
21
- import CardBase from './CardBase'
21
+ import CardBase, { fullBleedContentPropTypes } from './CardBase'
22
22
 
23
23
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([
24
24
  a11yProps,
@@ -37,6 +37,10 @@ const tokenKeys = [
37
37
  'paddingLeft',
38
38
  'paddingRight',
39
39
  'paddingTop',
40
+ 'marginTop',
41
+ 'marginBottom',
42
+ 'marginLeft',
43
+ 'marginRight',
40
44
  'minWidth',
41
45
  'shadow',
42
46
  'contentAlignItems',
@@ -81,6 +85,7 @@ const PressableCardBase = React.forwardRef(
81
85
  hrefAttrs,
82
86
  dataSet,
83
87
  backgroundImage,
88
+ fullBleedContent,
84
89
  accessibilityRole = href ? 'link' : undefined,
85
90
  ...rawRest
86
91
  },
@@ -159,13 +164,14 @@ const PressableCardBase = React.forwardRef(
159
164
  setFocused(false)
160
165
  setPressed(false)
161
166
  }}
162
- style={{ ...staticStyles.linkContainer, textDecoration: 'none' }}
167
+ style={staticStyles.linkContainer}
163
168
  {...(hrefAttrs || {})}
164
169
  role={accessibilityRole}
165
170
  >
166
171
  <CardBase
167
172
  tokens={getCardTokens({ pressed, focused, hovered })}
168
173
  backgroundImage={backgroundImage}
174
+ fullBleedContent={fullBleedContent}
169
175
  >
170
176
  {typeof children === 'function'
171
177
  ? children(getCardState({ pressed, focused, hovered }))
@@ -188,7 +194,11 @@ const PressableCardBase = React.forwardRef(
188
194
  {...selectProps({ ...rest, accessibilityRole })}
189
195
  >
190
196
  {(pressableState) => (
191
- <CardBase tokens={getCardTokens(pressableState)} backgroundImage={backgroundImage}>
197
+ <CardBase
198
+ tokens={getCardTokens(pressableState)}
199
+ backgroundImage={backgroundImage}
200
+ fullBleedContent={fullBleedContent}
201
+ >
192
202
  {typeof children === 'function' ? children(getCardState(pressableState)) : children}
193
203
  </CardBase>
194
204
  )}
@@ -206,7 +216,8 @@ const staticStyles = StyleSheet.create({
206
216
  flex: 1,
207
217
  display: 'flex',
208
218
  alignItems: 'stretch',
209
- justifyContent: 'flex-start'
219
+ justifyContent: 'flex-start',
220
+ textDecorationLine: 'none'
210
221
  }
211
222
  })
212
223
 
@@ -234,7 +245,8 @@ PressableCardBase.propTypes = {
234
245
  PropTypes.oneOf(['start', 'end', 'center', 'stretch']),
235
246
  PropTypes.object
236
247
  ])
237
- })
248
+ }),
249
+ fullBleedContent: fullBleedContentPropTypes
238
250
  }
239
251
 
240
252
  export default withLinkRouter(PressableCardBase)
@@ -8,6 +8,7 @@ import {
8
8
  getTokensSetPropType,
9
9
  selectSystemProps,
10
10
  selectTokens,
11
+ variantProp,
11
12
  viewProps
12
13
  } from '../utils'
13
14
  import ScrollViewEnd from './ScrollViewEnd'
@@ -38,7 +39,7 @@ const selectBorderStyles = (borderBottomWidth, borderBottomColor) => ({
38
39
  * @TODO revisit `ScrollButton` after IconButton is stable.
39
40
  */
40
41
  const HorizontalScroll = React.forwardRef(
41
- ({ ScrollButton, tokens, itemPositions, children, ...rest }, ref) => {
42
+ ({ ScrollButton, tokens, itemPositions, variant, children, ...rest }, ref) => {
42
43
  const {
43
44
  nextIcon,
44
45
  previousIcon,
@@ -74,8 +75,9 @@ const HorizontalScroll = React.forwardRef(
74
75
  }
75
76
 
76
77
  const scrollMax = Math.max(0, contentWidth - containerWidth)
77
- const showNextButton = scrollOffset < scrollMax
78
- const showPreviousButton = scrollOffset > 0
78
+ const hideNavigationButtons = variant?.hideNavigationButtons || false
79
+ const showNextButton = scrollOffset < scrollMax && !hideNavigationButtons
80
+ const showPreviousButton = scrollOffset > 0 && !hideNavigationButtons
79
81
 
80
82
  const scrollRef = React.useRef(null)
81
83
  const scrollTo = (targetX) => {
@@ -173,6 +175,7 @@ HorizontalScroll.propTypes = {
173
175
  itemPositions: itemPositionsPropType,
174
176
  ScrollButton: PropTypes.elementType,
175
177
  tokens: getTokensSetPropType(tokenKeys, { allowFunction: true }),
178
+ variant: variantProp.propType,
176
179
  children: PropTypes.node
177
180
  }
178
181
 
package/src/Icon/Icon.jsx CHANGED
@@ -14,6 +14,7 @@ const Icon = React.forwardRef(
14
14
  tokens,
15
15
  scalesWithText = false,
16
16
  style = {},
17
+ testID,
17
18
  dataSet
18
19
  },
19
20
  ref
@@ -45,6 +46,7 @@ const Icon = React.forwardRef(
45
46
  const getIconContentForMobile = () => {
46
47
  return (
47
48
  <View
49
+ testID={testID}
48
50
  style={{
49
51
  backgroundColor: themeTokens.backgroundColor,
50
52
  borderRadius: themeTokens.borderRadius,
@@ -58,6 +60,7 @@ const Icon = React.forwardRef(
58
60
 
59
61
  return Platform.OS === 'web' ? (
60
62
  <View
63
+ testID={testID}
61
64
  ref={ref}
62
65
  // eslint-disable-next-line react-native/no-inline-styles
63
66
  style={{
@@ -5,6 +5,7 @@ import { useThemeTokens } from '../ThemeProvider'
5
5
  import Icon from '../Icon'
6
6
  import Spacer from '../Spacer'
7
7
  import { useListboxContext } from './ListboxContext'
8
+ import { variantProp } from '../utils'
8
9
 
9
10
  const styles = StyleSheet.create({
10
11
  container: {
@@ -47,39 +48,45 @@ const selectContainerStyles = (tokens) => ({
47
48
  borderBottomColor: tokens.groupBorderBottomColor
48
49
  })
49
50
 
50
- const GroupControl = React.forwardRef(({ expanded, pressed, hover, focus, label, id }, ref) => {
51
- const { selectedId, setSelectedId } = useListboxContext()
52
- const tokens = useThemeTokens(
53
- 'Listbox',
54
- {},
55
- {},
56
- {
57
- expanded,
58
- pressed,
59
- hover,
60
- current: selectedId === id && id !== undefined,
61
- focus
62
- }
63
- )
51
+ const GroupControl = React.forwardRef(
52
+ ({ expanded, pressed, hover, focus, label, id, variant = {} }, ref) => {
53
+ const { selectedId, setSelectedId } = useListboxContext()
54
+ const isSecondLevel = variant?.secondLevel === true
55
+ const tokens = useThemeTokens(
56
+ 'Listbox',
57
+ variant,
58
+ {},
59
+ {
60
+ expanded,
61
+ pressed,
62
+ hover,
63
+ current: selectedId === id && id !== undefined,
64
+ focus,
65
+ secondLevel: isSecondLevel
66
+ }
67
+ )
64
68
 
65
- return (
66
- <View
67
- onPress={() => setSelectedId(id)}
68
- style={[styles.container, selectContainerStyles(tokens)]}
69
- ref={ref}
70
- >
71
- <Text style={selectTextStyles(tokens)}>{label}</Text>
72
- <Spacer space={1} direction="row" />
73
- {tokens.groupIcon && (
74
- <Icon
75
- icon={tokens.groupIcon}
76
- tokens={{ color: tokens.groupColor }}
77
- variant={{ size: 'micro' }}
78
- />
79
- )}
80
- </View>
81
- )
82
- })
69
+ const displayIcon = isSecondLevel ? tokens.secondLevelParentIcon : tokens.groupIcon
70
+
71
+ return (
72
+ <View
73
+ onPress={() => setSelectedId(id)}
74
+ style={[styles.container, selectContainerStyles(tokens)]}
75
+ ref={ref}
76
+ >
77
+ <Text style={selectTextStyles(tokens)}>{label}</Text>
78
+ <Spacer space={1} direction="row" />
79
+ {displayIcon && (
80
+ <Icon
81
+ icon={displayIcon}
82
+ tokens={{ color: tokens.groupColor }}
83
+ variant={{ size: 'micro' }}
84
+ />
85
+ )}
86
+ </View>
87
+ )
88
+ }
89
+ )
83
90
 
84
91
  GroupControl.displayName = 'GroupControl'
85
92
 
@@ -89,7 +96,8 @@ GroupControl.propTypes = {
89
96
  pressed: PropTypes.bool,
90
97
  hover: PropTypes.bool,
91
98
  focus: PropTypes.bool,
92
- label: PropTypes.string
99
+ label: PropTypes.string,
100
+ variant: variantProp.propType
93
101
  }
94
102
 
95
103
  export default GroupControl
@@ -8,11 +8,14 @@ import ListboxGroup from './ListboxGroup'
8
8
  import ListboxItem from './ListboxItem'
9
9
  import { ListboxContext } from './ListboxContext'
10
10
  import DropdownOverlay from './ListboxOverlay'
11
+ import defaultDictionary from './dictionary'
11
12
 
12
13
  const styles = StyleSheet.create({
13
14
  container: {
14
15
  padding: 0,
15
- margin: 0
16
+ margin: 0,
17
+ position: 'relative',
18
+ overflow: 'visible'
16
19
  }
17
20
  })
18
21
 
@@ -41,6 +44,8 @@ const Listbox = React.forwardRef(
41
44
  LinkRouter,
42
45
  itemRouterProps,
43
46
  onClose,
47
+ copy = 'en',
48
+ dictionary = defaultDictionary,
44
49
  variant,
45
50
  tokens,
46
51
  testID
@@ -49,6 +54,7 @@ const Listbox = React.forwardRef(
49
54
  ) => {
50
55
  const initialOpen = getInitialOpen(items, defaultSelectedId)
51
56
  const [selectedId, setSelectedId] = React.useState(defaultSelectedId)
57
+ const [activeSecondLevelGroup, setActiveSecondLevelGroup] = React.useState(null)
52
58
  const listboxTokens = useThemeTokens('Listbox', tokens, variant)
53
59
 
54
60
  // We need to keep track of each item's ref in order to be able to
@@ -102,8 +108,15 @@ const Listbox = React.forwardRef(
102
108
  return () => {}
103
109
  }, [onClose, handleKeydown])
104
110
 
111
+ const contextValue = {
112
+ selectedId,
113
+ setSelectedId,
114
+ activeSecondLevelGroup,
115
+ setActiveSecondLevelGroup
116
+ }
117
+
105
118
  return (
106
- <ListboxContext.Provider value={{ selectedId, setSelectedId }}>
119
+ <ListboxContext.Provider value={contextValue}>
107
120
  <ExpandCollapse initialOpen={initialOpen} maxOpen={1} ref={ref}>
108
121
  {(expandProps) => (
109
122
  <View
@@ -121,6 +134,10 @@ const Listbox = React.forwardRef(
121
134
  return currentItemRef
122
135
  }
123
136
 
137
+ if (!nestedItems && activeSecondLevelGroup) {
138
+ return null
139
+ }
140
+
124
141
  return nestedItems ? (
125
142
  <ListboxGroup
126
143
  {...item}
@@ -131,6 +148,11 @@ const Listbox = React.forwardRef(
131
148
  nextItemRef={itemRefs.current[index + 1] ?? null}
132
149
  ref={index === 0 ? firstItemRef : itemRef}
133
150
  key={itemId}
151
+ copy={copy}
152
+ dictionary={dictionary}
153
+ variant={variant}
154
+ tokens={tokens}
155
+ onClose={onClose}
134
156
  />
135
157
  ) : (
136
158
  <ListboxItem
@@ -142,6 +164,8 @@ const Listbox = React.forwardRef(
142
164
  prevItemRef={itemRefs.current[index - 1] ?? null}
143
165
  nextItemRef={itemRefs.current[index + 1] ?? null}
144
166
  ref={index === 0 ? firstItemRef : itemRef}
167
+ variant={variant}
168
+ tokens={tokens}
145
169
  />
146
170
  )
147
171
  })}
@@ -183,6 +207,21 @@ Listbox.propTypes = {
183
207
  * Test ID for testing
184
208
  */
185
209
  testID: PropTypes.string,
210
+ /**
211
+ * Select English or French copy
212
+ */
213
+ copy: PropTypes.oneOf(['en', 'fr']),
214
+ /**
215
+ * Override the default dictionary, by passing the complete dictionary object for `en` and `fr`
216
+ */
217
+ dictionary: PropTypes.shape({
218
+ en: PropTypes.shape({
219
+ closeMenu: PropTypes.string.isRequired
220
+ }),
221
+ fr: PropTypes.shape({
222
+ closeMenu: PropTypes.string.isRequired
223
+ })
224
+ }),
186
225
  /**
187
226
  * Listbox variant
188
227
  */