@telus-uds/components-base 3.12.2 → 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 (63) hide show
  1. package/CHANGELOG.md +37 -2
  2. package/lib/cjs/BaseProvider/index.js +4 -1
  3. package/lib/cjs/Button/ButtonDropdown.js +105 -12
  4. package/lib/cjs/Card/Card.js +23 -4
  5. package/lib/cjs/Card/CardBase.js +170 -19
  6. package/lib/cjs/Card/PressableCardBase.js +19 -5
  7. package/lib/cjs/Card/backgroundImageStylesMap.js +197 -0
  8. package/lib/cjs/ExpandCollapse/ExpandCollapse.js +3 -1
  9. package/lib/cjs/ExpandCollapseMini/ExpandCollapseMini.js +1 -1
  10. package/lib/cjs/ExpandCollapseMini/ExpandCollapseMiniControl.js +30 -6
  11. package/lib/cjs/FlexGrid/FlexGrid.js +71 -6
  12. package/lib/cjs/Icon/Icon.js +3 -1
  13. package/lib/cjs/InputLabel/InputLabel.js +1 -1
  14. package/lib/cjs/InputSupports/InputSupports.js +1 -1
  15. package/lib/cjs/Notification/Notification.js +27 -8
  16. package/lib/cjs/Tabs/Tabs.js +34 -2
  17. package/lib/cjs/Tabs/TabsDropdown.js +252 -0
  18. package/lib/cjs/Tabs/TabsItem.js +4 -2
  19. package/lib/cjs/Tabs/dictionary.js +14 -0
  20. package/lib/cjs/ViewportProvider/ViewportProvider.js +9 -3
  21. package/lib/cjs/utils/props/inputSupportsProps.js +1 -1
  22. package/lib/esm/BaseProvider/index.js +4 -1
  23. package/lib/esm/Button/ButtonDropdown.js +107 -14
  24. package/lib/esm/Card/Card.js +21 -4
  25. package/lib/esm/Card/CardBase.js +169 -19
  26. package/lib/esm/Card/PressableCardBase.js +19 -5
  27. package/lib/esm/Card/backgroundImageStylesMap.js +190 -0
  28. package/lib/esm/ExpandCollapse/ExpandCollapse.js +4 -2
  29. package/lib/esm/ExpandCollapseMini/ExpandCollapseMini.js +2 -2
  30. package/lib/esm/ExpandCollapseMini/ExpandCollapseMiniControl.js +30 -6
  31. package/lib/esm/FlexGrid/FlexGrid.js +72 -7
  32. package/lib/esm/Icon/Icon.js +3 -1
  33. package/lib/esm/InputLabel/InputLabel.js +1 -1
  34. package/lib/esm/InputSupports/InputSupports.js +1 -1
  35. package/lib/esm/Notification/Notification.js +27 -8
  36. package/lib/esm/Tabs/Tabs.js +35 -3
  37. package/lib/esm/Tabs/TabsDropdown.js +245 -0
  38. package/lib/esm/Tabs/TabsItem.js +4 -2
  39. package/lib/esm/Tabs/dictionary.js +8 -0
  40. package/lib/esm/ViewportProvider/ViewportProvider.js +9 -3
  41. package/lib/esm/utils/props/inputSupportsProps.js +1 -1
  42. package/lib/package.json +2 -2
  43. package/package.json +2 -2
  44. package/src/BaseProvider/index.jsx +4 -2
  45. package/src/Button/ButtonDropdown.jsx +109 -16
  46. package/src/Card/Card.jsx +27 -3
  47. package/src/Card/CardBase.jsx +165 -19
  48. package/src/Card/PressableCardBase.jsx +31 -4
  49. package/src/Card/backgroundImageStylesMap.js +41 -0
  50. package/src/ExpandCollapse/ExpandCollapse.jsx +5 -2
  51. package/src/ExpandCollapseMini/ExpandCollapseMini.jsx +2 -2
  52. package/src/ExpandCollapseMini/ExpandCollapseMiniControl.jsx +39 -9
  53. package/src/FlexGrid/FlexGrid.jsx +80 -7
  54. package/src/Icon/Icon.jsx +3 -1
  55. package/src/InputLabel/InputLabel.jsx +1 -1
  56. package/src/InputSupports/InputSupports.jsx +1 -1
  57. package/src/Notification/Notification.jsx +58 -9
  58. package/src/Tabs/Tabs.jsx +36 -2
  59. package/src/Tabs/TabsDropdown.jsx +265 -0
  60. package/src/Tabs/TabsItem.jsx +4 -2
  61. package/src/Tabs/dictionary.js +8 -0
  62. package/src/ViewportProvider/ViewportProvider.jsx +8 -3
  63. package/src/utils/props/inputSupportsProps.js +1 -1
@@ -42,10 +42,15 @@ const ExpandCollapseMiniControl = React.forwardRef(
42
42
  pressed
43
43
  }
44
44
  )
45
- const { size, icon } = useThemeTokens('ExpandCollapseMiniControl', tokens, variant, {
46
- expanded,
47
- focus
48
- })
45
+ const { fontSize, lineHeight, iconSize, icon } = useThemeTokens(
46
+ 'ExpandCollapseMiniControl',
47
+ tokens,
48
+ variant,
49
+ {
50
+ expanded,
51
+ focus
52
+ }
53
+ )
49
54
 
50
55
  // Choose hover styles when any part of Control is hoverred
51
56
  const appearance = { ...variant, hover }
@@ -54,16 +59,39 @@ const ExpandCollapseMiniControl = React.forwardRef(
54
59
  const { hover: linkHover } = linkState || {}
55
60
  const isHovered = hover || linkHover
56
61
 
62
+ const iconBaselineOffset = 0
63
+ const hoverTranslateY = 4
64
+
65
+ // Calculate baseline alignment to vertically center icon with text
66
+ // This combines font and icon metrics with adjustments for visual balance
67
+ const fontBaseline = fontSize / hoverTranslateY // Quarter of font size - adjusts for text's visual center point
68
+ const iconBaseline = iconSize / hoverTranslateY // Quarter of icon size - adjusts for icon's visual center point
69
+ const staticOffset = hoverTranslateY // Fixed downward adjustment to fine-tune vertical alignment
70
+ const sizeCompensation = -Math.abs(iconSize - fontSize) // Compensates when icon and text sizes differ significantly
71
+
72
+ const baselineAlignment = fontBaseline + iconBaseline - staticOffset + sizeCompensation
73
+
57
74
  if (Platform.OS !== 'web') {
58
- return { iconTranslateY: -1 }
75
+ // For native platforms, use baseline alignment with optional offset
76
+ return { iconTranslateY: baselineAlignment + iconBaselineOffset }
59
77
  }
60
78
 
61
79
  if (isHovered) {
62
- // Include vertical icon animation on hover alongside built-in Link theme, the size is size4
63
- return { iconTranslateY: (expanded ? -1 : 1) * size }
80
+ // Apply animation offset to the baseline-aligned position
81
+ // When expanded: move icon UP (1.3 the hover distance for clear movement)
82
+ // When collapsed: move icon DOWN (single hover distance)
83
+ const hoverMovementDistance = 1.3
84
+ const animationOffset = expanded
85
+ ? -(hoverTranslateY * hoverMovementDistance)
86
+ : hoverTranslateY
87
+
88
+ return {
89
+ iconTranslateY: baselineAlignment + iconBaselineOffset + animationOffset
90
+ }
64
91
  }
65
92
 
66
- return {}
93
+ // Default state uses baseline alignment with optional offset
94
+ return { iconTranslateY: baselineAlignment + iconBaselineOffset }
67
95
  }
68
96
 
69
97
  return (
@@ -73,7 +101,9 @@ const ExpandCollapseMiniControl = React.forwardRef(
73
101
  iconPosition={iconPosition}
74
102
  tokens={(linkState) => ({
75
103
  ...linkTokens,
76
- ...getTokens(linkState)
104
+ ...getTokens(linkState),
105
+ iconSize,
106
+ blockLineHeight: lineHeight
77
107
  })}
78
108
  ref={ref}
79
109
  {...presentationOnly}
@@ -10,7 +10,8 @@ import {
10
10
  selectSystemProps,
11
11
  BaseView,
12
12
  StyleSheet,
13
- createMediaQueryStyles
13
+ createMediaQueryStyles,
14
+ useResponsiveProp
14
15
  } from '../utils'
15
16
  import Row from './Row'
16
17
  import Col from './Col'
@@ -21,6 +22,54 @@ import { useViewport } from '../ViewportProvider'
21
22
 
22
23
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
23
24
 
25
+ const CONTENT_MAX_WIDTH = 'max'
26
+ const CONTENT_FULL_WIDTH = 'full'
27
+
28
+ /**
29
+ * Resolves the maximum width for content based on the provided value and responsive width.
30
+ *
31
+ * @param {number|string|null|undefined} contentMinWidthValue - The minimum width value for the content.
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
+ */
36
+ const resolveContentMaxWidth = (contentMinWidthValue, responsiveWidth) => {
37
+ if (!contentMinWidthValue || contentMinWidthValue === CONTENT_FULL_WIDTH) {
38
+ return null
39
+ }
40
+
41
+ if (Number.isFinite(contentMinWidthValue)) {
42
+ return contentMinWidthValue
43
+ }
44
+
45
+ if (contentMinWidthValue === CONTENT_MAX_WIDTH) {
46
+ return responsiveWidth
47
+ }
48
+
49
+ return contentMinWidthValue
50
+ }
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
+
24
73
  /**
25
74
  * A mobile-first flexbox grid.
26
75
  */
@@ -28,7 +77,7 @@ const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, vie
28
77
  const FlexGrid = React.forwardRef(
29
78
  (
30
79
  {
31
- limitWidth = true,
80
+ limitWidth = false,
32
81
  gutter = true,
33
82
  outsideGutter = true,
34
83
  xsReverse,
@@ -40,6 +89,7 @@ const FlexGrid = React.forwardRef(
40
89
  accessibilityRole,
41
90
  children,
42
91
  dataSet,
92
+ contentMinWidth,
43
93
  ...rest
44
94
  },
45
95
  ref
@@ -47,30 +97,36 @@ const FlexGrid = React.forwardRef(
47
97
  const reverseLevel = applyInheritance([xsReverse, smReverse, mdReverse, lgReverse, xlReverse])
48
98
  const viewport = useViewport()
49
99
  const {
100
+ themeOptions,
50
101
  themeOptions: { enableMediaQueryStyleSheet }
51
102
  } = useTheme()
52
103
 
53
104
  let flexgridStyles
54
105
  let mediaIds
55
106
 
107
+ const contentMinWidthValue = useResponsiveProp(contentMinWidth)
108
+ const responsiveWidth = useResponsiveProp(themeOptions?.contentMaxWidth)
109
+ const maxWidth = resolveContentMaxWidth(contentMinWidthValue, responsiveWidth)
110
+
56
111
  const stylesByViewport = {
57
112
  xs: {
113
+ maxWidth: getMaxWidthForViewport('xs', limitWidth, contentMinWidth, maxWidth),
58
114
  flexDirection: reverseLevel[0] ? 'column-reverse' : 'column'
59
115
  },
60
116
  sm: {
61
- maxWidth: limitWidth && viewports.map.get('sm'),
117
+ maxWidth: getMaxWidthForViewport('sm', limitWidth, contentMinWidth, maxWidth),
62
118
  flexDirection: reverseLevel[1] ? 'column-reverse' : 'column'
63
119
  },
64
120
  md: {
65
- maxWidth: limitWidth && viewports.map.get('md'),
121
+ maxWidth: getMaxWidthForViewport('md', limitWidth, contentMinWidth, maxWidth),
66
122
  flexDirection: reverseLevel[2] ? 'column-reverse' : 'column'
67
123
  },
68
124
  lg: {
69
- maxWidth: limitWidth && viewports.map.get('lg'),
125
+ maxWidth: getMaxWidthForViewport('lg', limitWidth, contentMinWidth, maxWidth),
70
126
  flexDirection: reverseLevel[3] ? 'column-reverse' : 'column'
71
127
  },
72
128
  xl: {
73
- maxWidth: limitWidth && viewports.map.get('xl'),
129
+ maxWidth: getMaxWidthForViewport('xl', limitWidth, contentMinWidth, maxWidth),
74
130
  flexDirection: reverseLevel[4] ? 'column-reverse' : 'column'
75
131
  }
76
132
  }
@@ -162,7 +218,24 @@ FlexGrid.propTypes = {
162
218
  /**
163
219
  * The rows and columns of the Grid. Will typically be `FlexGrid.Row` and `FlexGrid.Col` components.
164
220
  */
165
- children: PropTypes.node.isRequired
221
+ children: PropTypes.node.isRequired,
222
+ /**
223
+ * The minimum width of the content in the FlexGrid.
224
+ * This prop accepts responsive values for different viewports. If a number is provided,
225
+ * it will be the max content width for the desired viewport.
226
+ * - `xs`: 'max' | 'full' | <number>
227
+ * - `sm`: 'max' | 'full' | <number>
228
+ * - `md`: 'max' | 'full' | <number>
229
+ * - `lg`: 'max' | 'full' | <number>
230
+ * - `xl`: 'max' | 'full' | <number>
231
+ */
232
+ contentMinWidth: PropTypes.shape({
233
+ xl: PropTypes.oneOfType([PropTypes.oneOf(['max', 'full']), PropTypes.number]),
234
+ lg: PropTypes.oneOfType([PropTypes.oneOf(['max', 'full']), PropTypes.number]),
235
+ md: PropTypes.oneOfType([PropTypes.oneOf(['max', 'full']), PropTypes.number]),
236
+ sm: PropTypes.oneOfType([PropTypes.oneOf(['max', 'full']), PropTypes.number]),
237
+ xs: PropTypes.oneOfType([PropTypes.oneOf(['max', 'full']), PropTypes.number])
238
+ })
166
239
  }
167
240
 
168
241
  FlexGrid.Row = Row
package/src/Icon/Icon.jsx CHANGED
@@ -38,7 +38,9 @@ const Icon = React.forwardRef(
38
38
  width: themeTokens.size + themeTokens.width * 2, // sets the diameter of the circle which is the size of the icon plus twice the general padding established to obtain a perfect circle
39
39
  height: themeTokens.size + themeTokens.width * 2
40
40
  }
41
- : {}
41
+ : {
42
+ padding: themeTokens.padding
43
+ }
42
44
 
43
45
  const getIconContentForMobile = () => {
44
46
  if (Object.keys(paddingStyles).length) {
@@ -161,7 +161,7 @@ InputLabel.propTypes = {
161
161
  /**
162
162
  * Content of an optional `Tooltip`. If set, a tooltip button will be shown next to the label.
163
163
  */
164
- tooltip: PropTypes.oneOfType([tooltipPropTypes, PropTypes.string]),
164
+ tooltip: PropTypes.oneOfType([PropTypes.shape(tooltipPropTypes), PropTypes.string]),
165
165
  /**
166
166
  * Current number of characterts of an input text.
167
167
  */
@@ -121,7 +121,7 @@ InputSupports.propTypes = {
121
121
  * 1. `tooltip` as a string - The content of the tooltip.
122
122
  * 2. `tooltip` as an object - Tooltip component props to be passed.
123
123
  */
124
- tooltip: PropTypes.oneOfType([tooltipPropTypes, PropTypes.string]),
124
+ tooltip: PropTypes.oneOfType([PropTypes.shape(tooltipPropTypes), PropTypes.string]),
125
125
  /**
126
126
  * Use to visually mark an input as valid or invalid.
127
127
  */
@@ -26,6 +26,8 @@ import useCopy from '../utils/useCopy'
26
26
  import dictionary from './dictionary'
27
27
  import { useViewport } from '../ViewportProvider'
28
28
 
29
+ const CONTENT_MAX_WIDTH = 'max'
30
+
29
31
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
30
32
 
31
33
  const selectContainerStyles = (tokens) => ({ ...tokens })
@@ -65,8 +67,14 @@ const selectDismissButtonContainerStyles = ({ dismissButtonGap }) => ({
65
67
  placeContent: 'start'
66
68
  })
67
69
 
68
- const selectContentContainerStyle = (themeTokens, maxWidth, system, viewport) => ({
69
- maxWidth: system && viewport === 'xl' ? maxWidth : '100%',
70
+ const selectContentContainerStyle = (
71
+ themeTokens,
72
+ maxWidth,
73
+ system,
74
+ viewport,
75
+ useContentMaxWidth
76
+ ) => ({
77
+ maxWidth: system && (useContentMaxWidth || viewport === 'xl') ? maxWidth : '100%',
70
78
  width: '100%',
71
79
  paddingRight: themeTokens?.containerPaddingRight,
72
80
  paddingLeft: themeTokens?.containerPaddingLeft
@@ -79,7 +87,8 @@ const getMediaQueryStyles = (
79
87
  mediaIdsRef,
80
88
  dismissible,
81
89
  viewport,
82
- system
90
+ system,
91
+ useContentMaxWidth
83
92
  ) => {
84
93
  const transformedSelectContainerStyles = Object.entries(themeTokens).reduce(
85
94
  (acc, [vp, viewportTokens]) => {
@@ -100,7 +109,7 @@ const getMediaQueryStyles = (
100
109
  const transformedSelectContentContainerStyles = Object.entries(themeTokens).reduce(
101
110
  (acc, [vp, viewportTokens]) => {
102
111
  acc[vp] = {
103
- ...selectContentContainerStyle(viewportTokens, maxWidth, system, vp),
112
+ ...selectContentContainerStyle(viewportTokens, maxWidth, system, vp, useContentMaxWidth),
104
113
  flexDirection: 'row',
105
114
  flexShrink: 1,
106
115
  justifyContent: 'space-between',
@@ -169,7 +178,15 @@ const getMediaQueryStyles = (
169
178
  }
170
179
  }
171
180
 
172
- const getDefaultStyles = (themeTokens, themeOptions, maxWidth, dismissible, viewport, system) => ({
181
+ const getDefaultStyles = (
182
+ themeTokens,
183
+ themeOptions,
184
+ maxWidth,
185
+ dismissible,
186
+ viewport,
187
+ system,
188
+ useContentMaxWidth
189
+ ) => ({
173
190
  containerStyles: {
174
191
  container: {
175
192
  flexDirection: 'column',
@@ -181,7 +198,7 @@ const getDefaultStyles = (themeTokens, themeOptions, maxWidth, dismissible, view
181
198
  flexDirection: 'row',
182
199
  flexShrink: 1,
183
200
  justifyContent: 'space-between',
184
- ...selectContentContainerStyle(themeTokens, maxWidth, system, viewport),
201
+ ...selectContentContainerStyle(themeTokens, maxWidth, system, viewport, useContentMaxWidth),
185
202
  ...(system && { alignSelf: 'center' })
186
203
  }
187
204
  },
@@ -251,7 +268,20 @@ const getDefaultStyles = (themeTokens, themeOptions, maxWidth, dismissible, view
251
268
  * Show system notifications at the top of the page, below the navigation, and expands the full-width of the viewport
252
269
  */
253
270
  const Notification = React.forwardRef(
254
- ({ children, system, dismissible, copy = 'en', tokens, variant, onDismiss, ...rest }, ref) => {
271
+ (
272
+ {
273
+ children,
274
+ system,
275
+ dismissible,
276
+ copy = 'en',
277
+ tokens,
278
+ variant,
279
+ onDismiss,
280
+ contentMinWidth,
281
+ ...rest
282
+ },
283
+ ref
284
+ ) => {
255
285
  const [isDismissed, setIsDismissed] = React.useState(false)
256
286
  const viewport = useViewport()
257
287
  const getCopy = useCopy({ dictionary, copy })
@@ -266,6 +296,7 @@ const Notification = React.forwardRef(
266
296
  system: isSystemEnabled,
267
297
  viewport
268
298
  })
299
+ const useContentMaxWidth = useResponsiveProp(contentMinWidth) === CONTENT_MAX_WIDTH
269
300
  const maxWidth = useResponsiveProp(
270
301
  themeOptions?.contentMaxWidth,
271
302
  viewports.map.get(viewports.xl)
@@ -300,7 +331,8 @@ const Notification = React.forwardRef(
300
331
  mediaIdsRef,
301
332
  dismissible,
302
333
  viewport,
303
- isSystemEnabled
334
+ isSystemEnabled,
335
+ useContentMaxWidth
304
336
  )
305
337
  } else {
306
338
  notificationComponentRef.current = getDefaultStyles(
@@ -309,7 +341,8 @@ const Notification = React.forwardRef(
309
341
  maxWidth,
310
342
  dismissible,
311
343
  viewport,
312
- isSystemEnabled
344
+ isSystemEnabled,
345
+ useContentMaxWidth
313
346
  )
314
347
  }
315
348
 
@@ -434,6 +467,22 @@ Notification.propTypes = {
434
467
  * Callback function called when the dismiss button is clicked
435
468
  */
436
469
  onDismiss: PropTypes.func,
470
+ /**
471
+ * The minimum width of the content in the Notification when using the system variant.
472
+ * This prop accepts responsive values for different viewports.
473
+ * - `xs`: 'max' | 'full'
474
+ * - `sm`: 'max' | 'full'
475
+ * - `md`: 'max' | 'full'
476
+ * - `lg`: 'max' | 'full'
477
+ * - `xl`: 'max' | 'full'
478
+ */
479
+ contentMinWidth: PropTypes.shape({
480
+ xl: PropTypes.oneOf(['max', 'full']),
481
+ lg: PropTypes.oneOf(['max', 'full']),
482
+ md: PropTypes.oneOf(['max', 'full']),
483
+ sm: PropTypes.oneOf(['max', 'full']),
484
+ xs: PropTypes.oneOf(['max', 'full'])
485
+ }),
437
486
  tokens: getTokensPropType('Notification'),
438
487
  variant: variantProp.propType
439
488
  }
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,