@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +10 -2
  2. package/lib/cjs/BaseProvider/index.js +4 -1
  3. package/lib/cjs/Card/Card.js +23 -4
  4. package/lib/cjs/Card/CardBase.js +170 -19
  5. package/lib/cjs/Card/PressableCardBase.js +19 -5
  6. package/lib/cjs/Card/backgroundImageStylesMap.js +197 -0
  7. package/lib/cjs/FlexGrid/FlexGrid.js +28 -12
  8. package/lib/cjs/Tabs/Tabs.js +34 -2
  9. package/lib/cjs/Tabs/TabsDropdown.js +252 -0
  10. package/lib/cjs/Tabs/TabsItem.js +4 -2
  11. package/lib/cjs/Tabs/dictionary.js +14 -0
  12. package/lib/cjs/ViewportProvider/ViewportProvider.js +9 -3
  13. package/lib/esm/BaseProvider/index.js +4 -1
  14. package/lib/esm/Card/Card.js +21 -4
  15. package/lib/esm/Card/CardBase.js +169 -19
  16. package/lib/esm/Card/PressableCardBase.js +19 -5
  17. package/lib/esm/Card/backgroundImageStylesMap.js +190 -0
  18. package/lib/esm/FlexGrid/FlexGrid.js +28 -12
  19. package/lib/esm/Tabs/Tabs.js +35 -3
  20. package/lib/esm/Tabs/TabsDropdown.js +245 -0
  21. package/lib/esm/Tabs/TabsItem.js +4 -2
  22. package/lib/esm/Tabs/dictionary.js +8 -0
  23. package/lib/esm/ViewportProvider/ViewportProvider.js +9 -3
  24. package/lib/package.json +2 -2
  25. package/package.json +2 -2
  26. package/src/BaseProvider/index.jsx +4 -2
  27. package/src/Card/Card.jsx +27 -3
  28. package/src/Card/CardBase.jsx +165 -19
  29. package/src/Card/PressableCardBase.jsx +31 -4
  30. package/src/Card/backgroundImageStylesMap.js +41 -0
  31. package/src/FlexGrid/FlexGrid.jsx +30 -13
  32. package/src/Tabs/Tabs.jsx +36 -2
  33. package/src/Tabs/TabsDropdown.jsx +265 -0
  34. package/src/Tabs/TabsItem.jsx +4 -2
  35. package/src/Tabs/dictionary.js +8 -0
  36. package/src/ViewportProvider/ViewportProvider.jsx +8 -3
@@ -3,15 +3,112 @@ import PropTypes from 'prop-types';
3
3
  import View from "react-native-web/dist/exports/View";
4
4
  import Platform from "react-native-web/dist/exports/Platform";
5
5
  import ImageBackground from "react-native-web/dist/exports/ImageBackground";
6
+ import Image from "react-native-web/dist/exports/Image";
6
7
  import StyleSheet from "react-native-web/dist/exports/StyleSheet";
7
8
  import { applyShadowToken } from '../ThemeProvider';
8
9
  import { getTokensPropType, responsiveProps, useResponsiveProp, formatImageSource } from '../utils';
9
10
  import { a11yProps, viewProps, selectSystemProps } from '../utils/props';
10
- import { jsx as _jsx } from "react/jsx-runtime";
11
+ import backgroundImageStylesMap from './backgroundImageStylesMap';
12
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
13
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps]);
14
+ const setBackgroundImage = _ref => {
15
+ let {
16
+ src,
17
+ alt,
18
+ backgroundImageResizeMode,
19
+ backgroundImagePosition,
20
+ backgroundImageAlign,
21
+ content,
22
+ cardStyle
23
+ } = _ref;
24
+ const borderRadius = cardStyle?.borderRadius || 0;
25
+ const borderWidth = cardStyle?.borderWidth || 0;
26
+ const adjustedBorderRadius = Math.max(0, borderRadius - borderWidth);
27
+
28
+ // For contain mode with position and align, use CSS background properties for web
29
+ if (backgroundImageResizeMode === 'contain' && backgroundImagePosition && backgroundImageAlign) {
30
+ const positionKey = `${backgroundImagePosition}-${backgroundImageAlign}`;
31
+ if (Platform.OS === 'web') {
32
+ // Create background position based on position and align
33
+ let backgroundPosition;
34
+ switch (positionKey) {
35
+ case 'top-start':
36
+ backgroundPosition = 'left top';
37
+ break;
38
+ case 'top-center':
39
+ backgroundPosition = 'center top';
40
+ break;
41
+ case 'top-end':
42
+ backgroundPosition = 'right top';
43
+ break;
44
+ case 'bottom-start':
45
+ backgroundPosition = 'left bottom';
46
+ break;
47
+ case 'bottom-center':
48
+ backgroundPosition = 'center bottom';
49
+ break;
50
+ case 'bottom-end':
51
+ backgroundPosition = 'right bottom';
52
+ break;
53
+ case 'left-center':
54
+ backgroundPosition = 'left center';
55
+ break;
56
+ case 'right-center':
57
+ backgroundPosition = 'right center';
58
+ break;
59
+ default:
60
+ backgroundPosition = 'center center';
61
+ }
62
+ const backgroundImageStyle = {
63
+ backgroundImage: `url(${src})`,
64
+ backgroundSize: 'contain',
65
+ backgroundRepeat: 'no-repeat',
66
+ backgroundPosition,
67
+ borderRadius: adjustedBorderRadius
68
+ };
69
+ return /*#__PURE__*/_jsx(View, {
70
+ style: [staticStyles.imageBackground, backgroundImageStyle],
71
+ role: "img",
72
+ "aria-label": alt,
73
+ children: content
74
+ });
75
+ }
76
+ // For React Native, apply positioning styles with full dimensions
77
+ const positionStyles = backgroundImageStylesMap[positionKey] || {};
78
+ return /*#__PURE__*/_jsxs(View, {
79
+ style: [staticStyles.containContainer, {
80
+ borderRadius: adjustedBorderRadius
81
+ }],
82
+ children: [/*#__PURE__*/_jsx(Image, {
83
+ source: src,
84
+ resizeMode: backgroundImageResizeMode,
85
+ style: [staticStyles.containImage, positionStyles],
86
+ accessible: true,
87
+ accessibilityLabel: alt,
88
+ accessibilityIgnoresInvertColors: true
89
+ }), /*#__PURE__*/_jsx(View, {
90
+ style: staticStyles.contentOverlay,
91
+ children: content
92
+ })]
93
+ });
94
+ }
95
+
96
+ // Use ImageBackground for all other resize modes and React Native
97
+ return /*#__PURE__*/_jsx(ImageBackground, {
98
+ source: src,
99
+ imageStyle: {
100
+ borderRadius: adjustedBorderRadius
101
+ },
102
+ resizeMode: backgroundImageResizeMode,
103
+ style: staticStyles.imageBackground,
104
+ accessible: true,
105
+ accessibilityLabel: alt,
106
+ children: content
107
+ });
108
+ };
12
109
 
13
110
  // Ensure explicit selection of tokens
14
- const selectStyles = _ref => {
111
+ export const selectStyles = _ref2 => {
15
112
  let {
16
113
  flex,
17
114
  backgroundColor,
@@ -28,7 +125,7 @@ const selectStyles = _ref => {
28
125
  gradient,
29
126
  maxHeight,
30
127
  overflowY
31
- } = _ref;
128
+ } = _ref2;
32
129
  return {
33
130
  flex,
34
131
  backgroundColor,
@@ -61,46 +158,97 @@ const selectStyles = _ref => {
61
158
  * A themeless base component for Card which components can apply theme tokens to. Not
62
159
  * intended to be used in apps or sites directly: build themed components on top of this.
63
160
  */
64
- const CardBase = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
161
+ const CardBase = /*#__PURE__*/React.forwardRef((_ref3, ref) => {
65
162
  let {
66
163
  children,
67
164
  tokens,
68
165
  dataSet,
69
166
  backgroundImage,
70
167
  ...rest
71
- } = _ref2;
168
+ } = _ref3;
72
169
  const cardStyle = selectStyles(typeof tokens === 'function' ? tokens() : tokens);
73
170
  const props = selectProps(rest);
171
+ let content = children;
74
172
  const {
75
173
  src = '',
76
174
  alt = '',
77
- resizeMode = ''
175
+ resizeMode = '',
176
+ position = '',
177
+ align = ''
78
178
  } = backgroundImage || {};
79
179
  const backgroundImageResizeMode = useResponsiveProp(resizeMode, 'cover');
180
+ const backgroundImagePosition = useResponsiveProp(position);
181
+ const backgroundImageAlign = useResponsiveProp(align);
80
182
  const imageSourceViewport = formatImageSource(useResponsiveProp(src));
183
+ if (backgroundImage && src) {
184
+ // When there's a background image, separate the padding from the container style
185
+ // so the image can fill the entire container without padding interference
186
+ const {
187
+ paddingTop,
188
+ paddingBottom,
189
+ paddingLeft,
190
+ paddingRight,
191
+ ...containerStyle
192
+ } = cardStyle;
193
+
194
+ // Only create padding wrapper if there's actually padding defined
195
+ const hasPadding = paddingTop || paddingBottom || paddingLeft || paddingRight;
196
+ const paddedContent = hasPadding ? /*#__PURE__*/_jsx(View, {
197
+ style: {
198
+ paddingTop,
199
+ paddingBottom,
200
+ paddingLeft,
201
+ paddingRight
202
+ },
203
+ children: children
204
+ }) : children;
205
+ content = setBackgroundImage({
206
+ src: imageSourceViewport,
207
+ alt,
208
+ backgroundImageResizeMode,
209
+ backgroundImagePosition,
210
+ backgroundImageAlign,
211
+ content: paddedContent,
212
+ cardStyle: containerStyle
213
+ });
214
+ return /*#__PURE__*/_jsx(View, {
215
+ style: containerStyle,
216
+ dataSet: dataSet,
217
+ ref: ref,
218
+ ...props,
219
+ children: content
220
+ });
221
+ }
81
222
  return /*#__PURE__*/_jsx(View, {
82
223
  style: cardStyle,
83
224
  dataSet: dataSet,
84
225
  ref: ref,
85
226
  ...props,
86
- children: src ? /*#__PURE__*/_jsx(ImageBackground, {
87
- source: imageSourceViewport,
88
- imageStyle: {
89
- borderRadius: cardStyle?.borderRadius - cardStyle?.borderWidth
90
- },
91
- resizeMode: backgroundImageResizeMode,
92
- style: styles.imageBackground,
93
- accessible: true,
94
- accessibilityLabel: alt,
95
- children: children
96
- }) : children
227
+ children: content
97
228
  });
98
229
  });
99
230
  CardBase.displayName = 'CardBase';
100
- const styles = StyleSheet.create({
231
+ const staticStyles = StyleSheet.create({
101
232
  imageBackground: {
102
233
  width: '100%',
103
234
  height: '100%'
235
+ },
236
+ contentOverlay: {
237
+ position: 'relative',
238
+ width: '100%',
239
+ height: '100%',
240
+ zIndex: 1
241
+ },
242
+ containContainer: {
243
+ width: '100%',
244
+ height: '100%',
245
+ overflow: 'hidden',
246
+ position: 'relative'
247
+ },
248
+ containImage: {
249
+ position: 'absolute',
250
+ width: '100%',
251
+ height: '100%'
104
252
  }
105
253
  });
106
254
  CardBase.propTypes = {
@@ -115,7 +263,9 @@ CardBase.propTypes = {
115
263
  // src is an object when used responsively to provide different image sources for different screen sizes
116
264
  src: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object]).isRequired,
117
265
  alt: PropTypes.string,
118
- resizeMode: responsiveProps.getTypeOptionallyByViewport(PropTypes.oneOf(['cover', 'contain', 'stretch', 'repeat', 'center']))
266
+ resizeMode: responsiveProps.getTypeOptionallyByViewport(PropTypes.oneOf(['cover', 'contain', 'stretch', 'repeat', 'center'])),
267
+ position: responsiveProps.getTypeOptionallyByViewport(PropTypes.oneOf(['bottom', 'left', 'right', 'top'])),
268
+ align: responsiveProps.getTypeOptionallyByViewport(PropTypes.oneOf(['start', 'end', 'center', 'stretch']))
119
269
  })
120
270
  };
121
271
  export default CardBase;
@@ -31,6 +31,7 @@ const PressableCardBase = /*#__PURE__*/React.forwardRef((_ref, ref) => {
31
31
  href,
32
32
  hrefAttrs,
33
33
  dataSet,
34
+ backgroundImage,
34
35
  accessibilityRole = href ? 'link' : undefined,
35
36
  ...rawRest
36
37
  } = _ref;
@@ -125,10 +126,7 @@ const PressableCardBase = /*#__PURE__*/React.forwardRef((_ref, ref) => {
125
126
  setFocused(false);
126
127
  setPressed(false);
127
128
  },
128
- style: {
129
- ...staticStyles.container,
130
- textDecoration: 'none'
131
- },
129
+ style: staticStyles.linkContainer,
132
130
  ...(hrefAttrs || {}),
133
131
  children: /*#__PURE__*/_jsx(CardBase, {
134
132
  tokens: getCardTokens({
@@ -136,6 +134,7 @@ const PressableCardBase = /*#__PURE__*/React.forwardRef((_ref, ref) => {
136
134
  focused,
137
135
  hovered
138
136
  }),
137
+ backgroundImage: backgroundImage,
139
138
  children: typeof children === 'function' ? children(getCardState({
140
139
  pressed,
141
140
  focused,
@@ -159,6 +158,7 @@ const PressableCardBase = /*#__PURE__*/React.forwardRef((_ref, ref) => {
159
158
  }),
160
159
  children: pressableState => /*#__PURE__*/_jsx(CardBase, {
161
160
  tokens: getCardTokens(pressableState),
161
+ backgroundImage: backgroundImage,
162
162
  children: typeof children === 'function' ? children(getCardState(pressableState)) : children
163
163
  })
164
164
  });
@@ -167,6 +167,11 @@ const staticStyles = StyleSheet.create({
167
167
  container: {
168
168
  flex: 1,
169
169
  display: 'flex'
170
+ },
171
+ linkContainer: {
172
+ flex: 1,
173
+ display: 'flex',
174
+ textDecoration: 'none'
170
175
  }
171
176
  });
172
177
  PressableCardBase.displayName = 'PressableCardBase';
@@ -177,6 +182,15 @@ PressableCardBase.propTypes = {
177
182
  partial: true,
178
183
  allowFunction: true
179
184
  }),
180
- variant: variantProp.propType
185
+ variant: variantProp.propType,
186
+ backgroundImage: PropTypes.shape({
187
+ // The image src is either a URI string or a number (when a local image src is bundled in IOS or Android app)
188
+ // src is an object when used responsively to provide different image sources for different screen sizes
189
+ src: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object]).isRequired,
190
+ alt: PropTypes.string,
191
+ resizeMode: PropTypes.oneOfType([PropTypes.oneOf(['cover', 'contain', 'stretch', 'repeat', 'center']), PropTypes.object]),
192
+ position: PropTypes.oneOfType([PropTypes.oneOf(['bottom', 'left', 'right', 'top']), PropTypes.object]),
193
+ align: PropTypes.oneOfType([PropTypes.oneOf(['start', 'end', 'center', 'stretch']), PropTypes.object])
194
+ })
181
195
  };
182
196
  export default withLinkRouter(PressableCardBase);
@@ -0,0 +1,190 @@
1
+ import Platform from "react-native-web/dist/exports/Platform";
2
+ const webStyles = {
3
+ 'top-start': {
4
+ top: 0,
5
+ left: 0
6
+ },
7
+ 'top-center': {
8
+ top: 0,
9
+ left: '50%',
10
+ transform: [{
11
+ translateX: '-50%'
12
+ }]
13
+ },
14
+ 'top-end': {
15
+ top: 0,
16
+ right: 0
17
+ },
18
+ 'right-start': {
19
+ top: 0,
20
+ right: 0
21
+ },
22
+ 'left-start': {
23
+ top: 0,
24
+ left: 0
25
+ },
26
+ 'left-center': {
27
+ top: '50%',
28
+ left: 0,
29
+ transform: [{
30
+ translateY: '-50%'
31
+ }]
32
+ },
33
+ 'right-center': {
34
+ top: '50%',
35
+ right: 0,
36
+ transform: [{
37
+ translateY: '-50%'
38
+ }]
39
+ },
40
+ 'bottom-start': {
41
+ bottom: 0,
42
+ left: 0
43
+ },
44
+ 'left-end': {
45
+ bottom: 0,
46
+ left: 0
47
+ },
48
+ 'bottom-center': {
49
+ bottom: 0,
50
+ left: '50%',
51
+ transform: [{
52
+ translateX: '-50%'
53
+ }]
54
+ },
55
+ 'bottom-end': {
56
+ bottom: 0,
57
+ right: 0
58
+ },
59
+ 'right-end': {
60
+ bottom: 0,
61
+ right: 0
62
+ },
63
+ 'top-stretch': {
64
+ top: 0,
65
+ left: 0,
66
+ right: 0,
67
+ width: '100%'
68
+ },
69
+ 'left-stretch': {
70
+ top: 0,
71
+ bottom: 0,
72
+ left: 0,
73
+ height: '100%'
74
+ },
75
+ 'right-stretch': {
76
+ top: 0,
77
+ bottom: 0,
78
+ right: 0,
79
+ height: '100%'
80
+ },
81
+ 'bottom-stretch': {
82
+ bottom: 0,
83
+ left: 0,
84
+ right: 0,
85
+ width: '100%'
86
+ }
87
+ };
88
+ const nativeStyles = {
89
+ 'top-start': {
90
+ top: 0,
91
+ left: 0,
92
+ width: 150,
93
+ height: 200
94
+ },
95
+ 'top-center': {
96
+ top: 0,
97
+ left: '50%',
98
+ marginLeft: -75,
99
+ width: 150,
100
+ height: 200
101
+ },
102
+ 'top-end': {
103
+ top: 0,
104
+ right: 0,
105
+ width: 150,
106
+ height: 200
107
+ },
108
+ 'right-start': {
109
+ top: 0,
110
+ right: 0,
111
+ width: 150,
112
+ height: 200
113
+ },
114
+ 'left-start': {
115
+ top: 0,
116
+ left: 0,
117
+ width: 150,
118
+ height: 200
119
+ },
120
+ 'left-center': {
121
+ left: 0,
122
+ top: '50%',
123
+ marginTop: -100,
124
+ width: 150,
125
+ height: 200
126
+ },
127
+ 'right-center': {
128
+ right: 0,
129
+ top: '50%',
130
+ marginTop: -100,
131
+ width: 150,
132
+ height: 200
133
+ },
134
+ 'bottom-start': {
135
+ bottom: 0,
136
+ left: 0,
137
+ width: 150,
138
+ height: 200
139
+ },
140
+ 'left-end': {
141
+ bottom: 0,
142
+ left: 0,
143
+ width: 150,
144
+ height: 200
145
+ },
146
+ 'bottom-center': {
147
+ bottom: 0,
148
+ left: '50%',
149
+ marginLeft: -75,
150
+ width: 150,
151
+ height: 200
152
+ },
153
+ 'bottom-end': {
154
+ bottom: 0,
155
+ right: 0,
156
+ width: 150,
157
+ height: 200
158
+ },
159
+ 'right-end': {
160
+ bottom: 0,
161
+ right: 0,
162
+ width: 150,
163
+ height: 200
164
+ },
165
+ 'top-stretch': {
166
+ top: 0,
167
+ left: 0,
168
+ right: 0,
169
+ width: '100%'
170
+ },
171
+ 'left-stretch': {
172
+ top: 0,
173
+ bottom: 0,
174
+ left: 0,
175
+ height: '100%'
176
+ },
177
+ 'right-stretch': {
178
+ top: 0,
179
+ bottom: 0,
180
+ right: 0,
181
+ height: '100%'
182
+ },
183
+ 'bottom-stretch': {
184
+ bottom: 0,
185
+ left: 0,
186
+ right: 0,
187
+ width: '100%'
188
+ }
189
+ };
190
+ export default Platform.OS === 'web' ? webStyles : nativeStyles;
@@ -17,17 +17,14 @@ const CONTENT_FULL_WIDTH = 'full';
17
17
  * Resolves the maximum width for content based on the provided value and responsive width.
18
18
  *
19
19
  * @param {number|string|null|undefined} contentMinWidthValue - The minimum width value for the content.
20
- * Can be a number (pixels), a string constant (e.g., CONTENT_FULL_WIDTH, CONTENT_MAX_WIDTH), or null/undefined.
21
- * @param {number|string} responsiveWidth - The responsive width to use when contentMinWidthValue is CONTENT_MAX_WIDTH.
22
- * @returns {number|string|null} The resolved maximum width value, which can be a number, a string (e.g., '100%'), or null.
20
+ * Can be a number, a special string constant (e.g., CONTENT_FULL_WIDTH, CONTENT_MAX_WIDTH), or null/undefined.
21
+ * @param {number} responsiveWidth - The responsive width to use when contentMinWidthValue is CONTENT_MAX_WIDTH.
22
+ * @returns {number|string|null} The resolved maximum width value, or null if full width is desired.
23
23
  */
24
24
  const resolveContentMaxWidth = (contentMinWidthValue, responsiveWidth) => {
25
- if (!contentMinWidthValue) {
25
+ if (!contentMinWidthValue || contentMinWidthValue === CONTENT_FULL_WIDTH) {
26
26
  return null;
27
27
  }
28
- if (contentMinWidthValue === CONTENT_FULL_WIDTH) {
29
- return '100%';
30
- }
31
28
  if (Number.isFinite(contentMinWidthValue)) {
32
29
  return contentMinWidthValue;
33
30
  }
@@ -37,6 +34,25 @@ const resolveContentMaxWidth = (contentMinWidthValue, responsiveWidth) => {
37
34
  return contentMinWidthValue;
38
35
  };
39
36
 
37
+ /**
38
+ * Calculates the maximum width for a given viewport based on limitWidth and contentMinWidth settings.
39
+ *
40
+ * @param {string} viewportKey - The viewport key ('xs', 'sm', 'md', 'lg', 'xl')
41
+ * @param {boolean} limitWidth - Whether to limit the width to viewport breakpoints
42
+ * @param {any} contentMinWidth - The contentMinWidth prop value
43
+ * @param {number|string|null} maxWidth - The resolved max width value
44
+ * @returns {number|string|null} The calculated maximum width for the viewport
45
+ */
46
+ const getMaxWidthForViewport = (viewportKey, limitWidth, contentMinWidth, maxWidth) => {
47
+ if (limitWidth) {
48
+ return viewports.map.get(viewportKey === 'xs' ? 'sm' : viewportKey);
49
+ }
50
+ if (contentMinWidth) {
51
+ return maxWidth;
52
+ }
53
+ return viewportKey === 'xl' ? viewports.map.get('xl') : null;
54
+ };
55
+
40
56
  /**
41
57
  * A mobile-first flexbox grid.
42
58
  */
@@ -73,23 +89,23 @@ const FlexGrid = /*#__PURE__*/React.forwardRef((_ref, ref) => {
73
89
  const maxWidth = resolveContentMaxWidth(contentMinWidthValue, responsiveWidth);
74
90
  const stylesByViewport = {
75
91
  xs: {
76
- maxWidth: limitWidth ? viewports.map.get('sm') : maxWidth,
92
+ maxWidth: getMaxWidthForViewport('xs', limitWidth, contentMinWidth, maxWidth),
77
93
  flexDirection: reverseLevel[0] ? 'column-reverse' : 'column'
78
94
  },
79
95
  sm: {
80
- maxWidth: limitWidth ? viewports.map.get('sm') : maxWidth,
96
+ maxWidth: getMaxWidthForViewport('sm', limitWidth, contentMinWidth, maxWidth),
81
97
  flexDirection: reverseLevel[1] ? 'column-reverse' : 'column'
82
98
  },
83
99
  md: {
84
- maxWidth: limitWidth ? viewports.map.get('md') : maxWidth,
100
+ maxWidth: getMaxWidthForViewport('md', limitWidth, contentMinWidth, maxWidth),
85
101
  flexDirection: reverseLevel[2] ? 'column-reverse' : 'column'
86
102
  },
87
103
  lg: {
88
- maxWidth: limitWidth ? viewports.map.get('lg') : maxWidth,
104
+ maxWidth: getMaxWidthForViewport('lg', limitWidth, contentMinWidth, maxWidth),
89
105
  flexDirection: reverseLevel[3] ? 'column-reverse' : 'column'
90
106
  },
91
107
  xl: {
92
- maxWidth: limitWidth ? viewports.map.get('xl') : maxWidth,
108
+ maxWidth: getMaxWidthForViewport('xl', limitWidth, contentMinWidth, maxWidth),
93
109
  flexDirection: reverseLevel[4] ? 'column-reverse' : 'column'
94
110
  }
95
111
  };
@@ -4,9 +4,10 @@ import PropTypes from 'prop-types';
4
4
  import ABBPropTypes from 'airbnb-prop-types';
5
5
  import { useThemeTokens } from '../ThemeProvider';
6
6
  import StackView from '../StackView';
7
- import { a11yProps, getTokensPropType, focusHandlerProps, pressProps, selectSystemProps, useHash, useInputValue, variantProp, viewProps, withLinkRouter } from '../utils';
7
+ import { a11yProps, getTokensPropType, focusHandlerProps, pressProps, selectSystemProps, useHash, useInputValue, useResponsiveProp, variantProp, viewProps, withLinkRouter } from '../utils';
8
8
  import HorizontalScroll, { horizontalScrollUtils, HorizontalScrollButton } from '../HorizontalScroll';
9
9
  import TabsItem from './TabsItem';
10
+ import TabsDropdown from './TabsDropdown';
10
11
  import { jsx as _jsx } from "react/jsx-runtime";
11
12
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps]);
12
13
  const [selectItemProps, selectedItemPropTypes] = selectSystemProps([a11yProps, focusHandlerProps, pressProps, viewProps]);
@@ -39,6 +40,12 @@ const getStackViewTokens = variant => {
39
40
  * Tabs renders a horizontally-scrolling menu of selectable buttons which may link
40
41
  * to a page or control what content is displayed on this page.
41
42
  *
43
+ * By default, Tabs always renders as horizontal scrolling tabs regardless of viewport.
44
+ * To enable dropdown mode, you must explicitly pass `variant={{ dropdown: true }}`.
45
+ * When dropdown is enabled, it will only render as a dropdown on mobile and tablet
46
+ * viewports (XS and SM). On larger viewports (MD, LG, XL), it will still render as
47
+ * horizontal tabs even with dropdown enabled.
48
+ *
42
49
  * If you are using Tabs to navigate to a new page (web-only) you should pass
43
50
  * `navigation`as the `accessibilityRole` to te Tabs component, this will cause
44
51
  * TabItems to default to a role of link and obtain aria-current behaviour.
@@ -84,6 +91,31 @@ const Tabs = /*#__PURE__*/React.forwardRef((_ref, ref) => {
84
91
  const parentAccessibilityRole = restProps.accessibilityRole ?? 'tablist';
85
92
  const defaultTabItemAccessibiltyRole = getDefaultTabItemAccessibilityRole(parentAccessibilityRole);
86
93
  const stackViewTokens = getStackViewTokens(variant);
94
+
95
+ // Render dropdown only if explicitly requested via variant AND viewport is xs or sm
96
+ const isSmallViewport = useResponsiveProp({
97
+ xs: true,
98
+ sm: true,
99
+ md: false,
100
+ lg: false,
101
+ xl: false
102
+ }, false);
103
+ const shouldRenderDropdown = variant?.dropdown === true && isSmallViewport;
104
+ if (shouldRenderDropdown) {
105
+ return /*#__PURE__*/_jsx(TabsDropdown, {
106
+ ref: ref,
107
+ tokens: tokens,
108
+ itemTokens: itemTokens,
109
+ variant: variant,
110
+ value: currentValue,
111
+ onChange: setValue,
112
+ items: items,
113
+ LinkRouter: LinkRouter,
114
+ linkRouterProps: linkRouterProps,
115
+ accessibilityRole: parentAccessibilityRole === 'tablist' ? 'button' : parentAccessibilityRole,
116
+ ...restProps
117
+ });
118
+ }
87
119
  return /*#__PURE__*/_jsx(HorizontalScroll, {
88
120
  ref: ref,
89
121
  ScrollButton: HorizontalScrollButton,
@@ -143,13 +175,13 @@ const Tabs = /*#__PURE__*/React.forwardRef((_ref, ref) => {
143
175
  Tabs.displayName = 'Tabs';
144
176
  Tabs.propTypes = {
145
177
  ...selectedSystemPropTypes,
146
- ...withLinkRouter.PropTypes,
178
+ ...withLinkRouter.propTypes,
147
179
  /**
148
180
  * Array of `TabsItem`s
149
181
  */
150
182
  items: PropTypes.arrayOf(PropTypes.shape({
151
183
  ...selectedItemPropTypes,
152
- ...withLinkRouter.PropTypes,
184
+ ...withLinkRouter.propTypes,
153
185
  href: PropTypes.string,
154
186
  label: PropTypes.string,
155
187
  id: PropTypes.string,