@telus-uds/components-base 3.17.1 → 3.19.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 (44) hide show
  1. package/CHANGELOG.md +20 -4
  2. package/LICENSE +21 -0
  3. package/jest.config.cjs +10 -2
  4. package/lib/cjs/Box/Box.js +114 -62
  5. package/lib/cjs/Box/backgroundImageStylesMap.js +136 -28
  6. package/lib/cjs/Carousel/Carousel.js +1 -1
  7. package/lib/cjs/Modal/Modal.js +7 -1
  8. package/lib/cjs/Modal/ModalContent.js +6 -4
  9. package/lib/cjs/MultiSelectFilter/MultiSelectFilter.js +2 -2
  10. package/lib/cjs/StepTracker/Step.js +12 -1
  11. package/lib/cjs/StepTracker/StepTracker.js +15 -4
  12. package/lib/cjs/TabBar/TabBar.js +4 -2
  13. package/lib/cjs/TabBar/index.js +2 -0
  14. package/lib/cjs/Tooltip/Backdrop.js +1 -1
  15. package/lib/cjs/utils/index.js +9 -1
  16. package/lib/cjs/utils/isTouchDevice.js +34 -0
  17. package/lib/esm/Box/Box.js +113 -63
  18. package/lib/esm/Box/backgroundImageStylesMap.js +134 -27
  19. package/lib/esm/Carousel/Carousel.js +2 -2
  20. package/lib/esm/Modal/Modal.js +7 -1
  21. package/lib/esm/Modal/ModalContent.js +6 -4
  22. package/lib/esm/MultiSelectFilter/MultiSelectFilter.js +2 -2
  23. package/lib/esm/StepTracker/Step.js +12 -1
  24. package/lib/esm/StepTracker/StepTracker.js +15 -4
  25. package/lib/esm/TabBar/TabBar.js +4 -2
  26. package/lib/esm/TabBar/index.js +2 -0
  27. package/lib/esm/Tooltip/Backdrop.js +1 -1
  28. package/lib/esm/utils/index.js +2 -1
  29. package/lib/esm/utils/isTouchDevice.js +27 -0
  30. package/lib/package.json +2 -2
  31. package/package.json +2 -2
  32. package/src/Box/Box.jsx +97 -55
  33. package/src/Box/backgroundImageStylesMap.js +48 -15
  34. package/src/Carousel/Carousel.jsx +3 -2
  35. package/src/Modal/Modal.jsx +7 -1
  36. package/src/Modal/ModalContent.jsx +6 -3
  37. package/src/MultiSelectFilter/MultiSelectFilter.jsx +2 -2
  38. package/src/StepTracker/Step.jsx +47 -27
  39. package/src/StepTracker/StepTracker.jsx +9 -1
  40. package/src/TabBar/TabBar.jsx +3 -1
  41. package/src/TabBar/index.js +3 -0
  42. package/src/Tooltip/Backdrop.jsx +1 -1
  43. package/src/utils/index.js +1 -0
  44. package/src/utils/isTouchDevice.js +34 -0
@@ -28,7 +28,8 @@ const ModalContent = /*#__PURE__*/React.forwardRef((_ref, ref) => {
28
28
  cancelButtonText,
29
29
  cancelButtonType: CancelButton = TextButton,
30
30
  children,
31
- onCancel
31
+ onCancel,
32
+ footer
32
33
  } = _ref;
33
34
  const viewport = useViewport();
34
35
  const {
@@ -142,7 +143,7 @@ const ModalContent = /*#__PURE__*/React.forwardRef((_ref, ref) => {
142
143
  children: /*#__PURE__*/_jsx(Typography, {
143
144
  children: bodyText
144
145
  })
145
- }), children, (hasConfirmButton || hasCancelButton) && /*#__PURE__*/_jsxs(View, {
146
+ }), children, (hasConfirmButton || hasCancelButton) && !footer && /*#__PURE__*/_jsxs(View, {
146
147
  style: [selectFooterContainerStyles({
147
148
  ...themeTokens,
148
149
  hasBorder: isContentOverflowing
@@ -164,7 +165,7 @@ const ModalContent = /*#__PURE__*/React.forwardRef((_ref, ref) => {
164
165
  children: cancelButtonText
165
166
  })
166
167
  }) : null]
167
- })]
168
+ }), footer]
168
169
  });
169
170
  });
170
171
  ModalContent.displayName = 'ModalContent';
@@ -193,6 +194,7 @@ ModalContent.propTypes = {
193
194
  cancelButtonType: PropTypes.elementType,
194
195
  // TODO: figure out a way of passing an icon to the TextButton
195
196
  children: PropTypes.node,
196
- onCancel: PropTypes.func
197
+ onCancel: PropTypes.func,
198
+ footer: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)])
197
199
  };
198
200
  export default ModalContent;
@@ -482,9 +482,9 @@ MultiSelectFilter.propTypes = {
482
482
  */
483
483
  label: PropTypes.string.isRequired,
484
484
  /**
485
- * The text for the subtitle
485
+ * The text for the subtitle. Can also be JSX.
486
486
  */
487
- subtitle: PropTypes.string,
487
+ subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
488
488
  /**
489
489
  * An optional unique string may be provided to identify the ButtonDropdown.
490
490
  * If not provided, the label is used.
@@ -150,6 +150,13 @@ const getStepTestID = (isCompleted, isCurrent) => {
150
150
  }
151
151
  return testID;
152
152
  };
153
+ const selectBarContainerStyles = (themeTokens, isCompleted, isCurrent) => ({
154
+ backgroundColor: isCompleted ? themeTokens.barCompletedBackgroundColor : themeTokens.barBackgroundColor,
155
+ height: themeTokens.barHeight,
156
+ ...(isCurrent && {
157
+ backgroundColor: themeTokens.barCurrentBackgroundColor
158
+ })
159
+ });
153
160
 
154
161
  /**
155
162
  * A single step of a StepTracker.
@@ -162,6 +169,7 @@ const Step = /*#__PURE__*/React.forwardRef((_ref8, ref) => {
162
169
  stepCount = 0,
163
170
  stepIndex = 0,
164
171
  tokens,
172
+ isBarVariant,
165
173
  ...rest
166
174
  } = _ref8;
167
175
  const {
@@ -190,7 +198,10 @@ const Step = /*#__PURE__*/React.forwardRef((_ref8, ref) => {
190
198
  accessibilityCurrent: status === stepIndex,
191
199
  ref: ref,
192
200
  ...selectProps(rest),
193
- children: [/*#__PURE__*/_jsxs(StackView, {
201
+ children: [isBarVariant && /*#__PURE__*/_jsx(View, {
202
+ style: selectBarContainerStyles(themeTokens, isCompleted, isCurrent),
203
+ testID: getStepTestID(isCompleted, isCurrent)
204
+ }), !isBarVariant && /*#__PURE__*/_jsxs(StackView, {
194
205
  direction: "row",
195
206
  space: 0,
196
207
  tokens: {
@@ -12,6 +12,7 @@ import useCopy from '../utils/useCopy';
12
12
  import Step from './Step';
13
13
  import defaultDictionary from './dictionary';
14
14
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
15
+ const STYLE_BAR_VARIANT = 'bar';
15
16
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps]);
16
17
  const selectContainerStyles = _ref => {
17
18
  let {
@@ -52,6 +53,14 @@ const selectStepTrackerLabelStyles = (_ref3, themeOptions) => {
52
53
  themeOptions
53
54
  });
54
55
  };
56
+ const selectStepsContainerStyles = _ref4 => {
57
+ let {
58
+ barGap
59
+ } = _ref4;
60
+ return {
61
+ gap: barGap
62
+ };
63
+ };
55
64
 
56
65
  /**
57
66
  * StepTracker component shows the current position in a sequence of steps.
@@ -85,7 +94,7 @@ const selectStepTrackerLabelStyles = (_ref3, themeOptions) => {
85
94
  * - `accessibilityValue.text` (`aria-valuetext`): `<Current Step Label>`,
86
95
  *
87
96
  */
88
- const StepTracker = /*#__PURE__*/React.forwardRef((_ref4, ref) => {
97
+ const StepTracker = /*#__PURE__*/React.forwardRef((_ref5, ref) => {
89
98
  let {
90
99
  current = 0,
91
100
  copy = 'en',
@@ -94,7 +103,8 @@ const StepTracker = /*#__PURE__*/React.forwardRef((_ref4, ref) => {
94
103
  tokens,
95
104
  variant,
96
105
  ...rest
97
- } = _ref4;
106
+ } = _ref5;
107
+ const isBarVariant = variant?.style === STYLE_BAR_VARIANT;
98
108
  const viewport = useViewport();
99
109
  const {
100
110
  showStepTrackerLabel,
@@ -140,7 +150,7 @@ const StepTracker = /*#__PURE__*/React.forwardRef((_ref4, ref) => {
140
150
  children: /*#__PURE__*/_jsxs(StackView, {
141
151
  space: 0,
142
152
  children: [/*#__PURE__*/_jsx(View, {
143
- style: staticStyles.stepsContainer,
153
+ style: [staticStyles.stepsContainer, selectStepsContainerStyles(themeTokens)],
144
154
  accessibilityRole: stepsContainerAccessibilityRole,
145
155
  children: steps.map((label, index) => {
146
156
  return /*#__PURE__*/_jsx(Step, {
@@ -151,7 +161,8 @@ const StepTracker = /*#__PURE__*/React.forwardRef((_ref4, ref) => {
151
161
  stepCount: steps.length,
152
162
  tokens: themeTokens,
153
163
  accessibilityRole: stepAccessibilityRole,
154
- accessibilityCurrent: current === index && Platform.OS === 'web' && 'step'
164
+ accessibilityCurrent: current === index && Platform.OS === 'web' && 'step',
165
+ isBarVariant: isBarVariant
155
166
  }, label);
156
167
  })
157
168
  }), showStepTrackerLabel && /*#__PURE__*/_jsx(View, {
@@ -84,7 +84,8 @@ const TabBar = /*#__PURE__*/React.forwardRef((_ref3, ref) => {
84
84
  iconActive: item.iconActive,
85
85
  onPress: () => handlePress(item.id),
86
86
  id: `tab-item-${index}`,
87
- accessibilityRole: "tablist"
87
+ accessibilityRole: "tablist",
88
+ tokens: item.tokens
88
89
  }, item.id))
89
90
  })
90
91
  });
@@ -98,7 +99,8 @@ TabBar.propTypes = {
98
99
  icon: PropTypes.node,
99
100
  iconActive: PropTypes.node,
100
101
  label: PropTypes.string.isRequired,
101
- href: PropTypes.string
102
+ href: PropTypes.string,
103
+ tokens: getTokensPropType('TabBarItem')
102
104
  })).isRequired,
103
105
  /** Id of the initially selected item. */
104
106
  initiallySelectedItem: PropTypes.number,
@@ -1,2 +1,4 @@
1
1
  import TabBar from './TabBar';
2
+ import TabBarItem from './TabBarItem';
3
+ TabBar.Item = TabBarItem;
2
4
  export default TabBar;
@@ -14,7 +14,7 @@ function createPortalNode(nodeId) {
14
14
  left: ${window.scrollX}px;
15
15
  right: 0;
16
16
  bottom: 0;
17
- z-index: 9999;
17
+ z-index: 100000;
18
18
  pointer-events: none;
19
19
  `;
20
20
  document.body.appendChild(node);
@@ -24,4 +24,5 @@ export { transformGradient } from './transformGradient';
24
24
  export { default as convertFromMegaByteToByte } from './convertFromMegaByteToByte';
25
25
  export { default as formatImageSource } from './formatImageSource';
26
26
  export { default as getSpacingScale } from './getSpacingScale';
27
- export { default as useVariants } from './useVariants';
27
+ export { default as useVariants } from './useVariants';
28
+ export { default as isTouchDevice } from './isTouchDevice';
@@ -0,0 +1,27 @@
1
+ import Platform from "react-native-web/dist/exports/Platform";
2
+ /**
3
+ * Determines if the current device supports touch interactions
4
+ *
5
+ * @returns {boolean} True if the device supports touch, false otherwise
6
+ */
7
+ const isTouchDevice = () => {
8
+ if (Platform.OS !== 'web') {
9
+ return true;
10
+ }
11
+ if (typeof window !== 'undefined') {
12
+ if ('ontouchstart' in window) {
13
+ return true;
14
+ }
15
+ if (window.navigator && window.navigator.maxTouchPoints > 0) {
16
+ return true;
17
+ }
18
+ if (window.navigator && window.navigator.msMaxTouchPoints > 0) {
19
+ return true;
20
+ }
21
+ if (window.matchMedia && window.matchMedia('(pointer: coarse)').matches) {
22
+ return true;
23
+ }
24
+ }
25
+ return false;
26
+ };
27
+ export default isTouchDevice;
package/lib/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  "@gorhom/portal": "^1.0.14",
13
13
  "@react-native-picker/picker": "^2.9.0",
14
14
  "@telus-uds/system-constants": "^3.0.0",
15
- "@telus-uds/system-theme-tokens": "^4.14.0",
15
+ "@telus-uds/system-theme-tokens": "^4.15.0",
16
16
  "airbnb-prop-types": "^2.16.0",
17
17
  "css-mediaquery": "^0.1.2",
18
18
  "expo-document-picker": "^13.0.1",
@@ -84,6 +84,6 @@
84
84
  "standard-engine": {
85
85
  "skip": true
86
86
  },
87
- "version": "3.17.1",
87
+ "version": "3.19.0",
88
88
  "types": "types/index.d.ts"
89
89
  }
package/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  "@gorhom/portal": "^1.0.14",
13
13
  "@react-native-picker/picker": "^2.9.0",
14
14
  "@telus-uds/system-constants": "^3.0.0",
15
- "@telus-uds/system-theme-tokens": "^4.14.0",
15
+ "@telus-uds/system-theme-tokens": "^4.15.0",
16
16
  "airbnb-prop-types": "^2.16.0",
17
17
  "css-mediaquery": "^0.1.2",
18
18
  "expo-document-picker": "^13.0.1",
@@ -84,6 +84,6 @@
84
84
  "standard-engine": {
85
85
  "skip": true
86
86
  },
87
- "version": "3.17.1",
87
+ "version": "3.19.0",
88
88
  "types": "types/index.d.ts"
89
89
  }
package/src/Box/Box.jsx CHANGED
@@ -21,9 +21,10 @@ import {
21
21
  variantProp,
22
22
  viewProps,
23
23
  StyleSheet as RNMQStyleSheet,
24
- getSpacingScale
24
+ getSpacingScale,
25
+ formatImageSource
25
26
  } from '../utils'
26
- import backgroundImageStylesMap from './backgroundImageStylesMap'
27
+ import backgroundImageStylesMap, { backgroundPositions } from './backgroundImageStylesMap'
27
28
  import { useViewport } from '../ViewportProvider'
28
29
 
29
30
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
@@ -87,38 +88,60 @@ const setBackgroundImage = ({
87
88
  backgroundImageResizeMode,
88
89
  backgroundImagePosition,
89
90
  backgroundImageAlign,
90
- backgroundImageWidth,
91
- backgroundImageHeight,
92
- content
91
+ content,
92
+ testID
93
93
  }) => {
94
- if (backgroundImageResizeMode === 'contain') {
95
- const containedViewStyle = {
96
- ...staticStyles.containedView,
97
- width: backgroundImageWidth,
98
- height: backgroundImageHeight,
99
- ...backgroundImageStylesMap[`${backgroundImagePosition}-${backgroundImageAlign}`]
94
+ const backgroundImageTestID = testID ? `${testID}-background-image` : undefined
95
+
96
+ if (backgroundImageResizeMode === 'contain' && backgroundImagePosition && backgroundImageAlign) {
97
+ const positionKey = `${backgroundImagePosition}-${backgroundImageAlign}`
98
+
99
+ if (Platform.OS === 'web') {
100
+ const backgroundPosition = backgroundPositions[positionKey] || 'center center'
101
+
102
+ const backgroundImageStyle = {
103
+ backgroundImage: `url(${src})`,
104
+ backgroundSize: 'contain',
105
+ backgroundRepeat: 'no-repeat',
106
+ backgroundPosition
107
+ }
108
+
109
+ return (
110
+ <View
111
+ style={[staticStyles.imageBackground, backgroundImageStyle]}
112
+ aria-label={alt}
113
+ testID={backgroundImageTestID}
114
+ >
115
+ {content}
116
+ </View>
117
+ )
100
118
  }
119
+ const positionStyles = backgroundImageStylesMap[positionKey] || {}
101
120
 
102
121
  return (
103
- <View style={staticStyles.containedContainer}>
104
- <View style={containedViewStyle}>
105
- <Image
106
- source={{ uri: src }}
107
- alt={alt}
108
- style={staticStyles.containedImage}
109
- accessibilityIgnoresInvertColors
110
- />
111
- </View>
112
- {content}
122
+ <View style={staticStyles.containContainer}>
123
+ <Image
124
+ source={src}
125
+ resizeMode={backgroundImageResizeMode}
126
+ style={[staticStyles.containImage, positionStyles]}
127
+ accessible={true}
128
+ accessibilityLabel={alt}
129
+ accessibilityIgnoresInvertColors={true}
130
+ testID={backgroundImageTestID}
131
+ />
132
+ <View style={staticStyles.contentOverlay}>{content}</View>
113
133
  </View>
114
134
  )
115
135
  }
136
+
116
137
  return (
117
138
  <ImageBackground
118
- source={{ uri: src }}
119
- alt={alt}
120
- style={staticStyles.backgroundImageContainer}
139
+ source={src}
121
140
  resizeMode={backgroundImageResizeMode}
141
+ style={staticStyles.imageBackground}
142
+ accessible={true}
143
+ accessibilityLabel={alt}
144
+ testID={backgroundImageTestID}
122
145
  >
123
146
  {content}
124
147
  </ImageBackground>
@@ -279,33 +302,47 @@ const Box = React.forwardRef(
279
302
 
280
303
  const { src = '', alt = '', resizeMode = '', position = '', align = '' } = backgroundImage || {}
281
304
  const backgroundImageResizeMode = useResponsiveProp(resizeMode, 'cover')
282
- const backgroundImagePosition = useResponsiveProp(position, 'none')
283
- const backgroundImageAlign = useResponsiveProp(align, 'stretch')
284
- const [backgroundImageWidth, setBackgroundImageWidth] = React.useState(0)
285
- const [backgroundImageHeight, setBackgroundImageHeight] = React.useState(0)
286
- if (backgroundImage)
305
+ const backgroundImagePosition = useResponsiveProp(position)
306
+ const backgroundImageAlign = useResponsiveProp(align)
307
+ const imageSourceViewport = formatImageSource(useResponsiveProp(src))
308
+
309
+ if (backgroundImage && src) {
310
+ const { paddingTop, paddingBottom, paddingLeft, paddingRight, ...containerStyle } = boxStyles
311
+
312
+ const hasPadding = paddingTop || paddingBottom || paddingLeft || paddingRight
313
+ const paddedContent = hasPadding ? (
314
+ <View style={{ paddingTop, paddingBottom, paddingLeft, paddingRight }}>{children}</View>
315
+ ) : (
316
+ children
317
+ )
318
+
287
319
  content = setBackgroundImage({
288
- src,
320
+ src: imageSourceViewport,
289
321
  alt,
290
322
  backgroundImageResizeMode,
291
323
  backgroundImagePosition,
292
324
  backgroundImageAlign,
293
- backgroundImageWidth,
294
- backgroundImageHeight,
295
- content
325
+ content: paddedContent,
326
+ testID
296
327
  })
297
328
 
298
- React.useEffect(() => {
299
- if (backgroundImage && backgroundImageWidth === 0 && backgroundImageHeight === 0) {
300
- Image.getSize(src, (width, height) => {
301
- // Only update the state if the size has changed
302
- if (width !== backgroundImageWidth || height !== backgroundImageHeight) {
303
- setBackgroundImageWidth(width)
304
- setBackgroundImageHeight(height)
305
- }
306
- })
329
+ const dataSetValue = boxMediaIds ? { media: boxMediaIds, ...dataSet } : dataSet
330
+
331
+ if (scroll) {
332
+ const scrollProps = typeof scroll === 'object' ? scroll : {}
333
+ scrollProps.contentContainerStyle = [containerStyle, scrollProps.contentContainerStyle]
334
+ return (
335
+ <ScrollView {...scrollProps} {...props} testID={testID} dataSet={dataSetValue} ref={ref}>
336
+ {content}
337
+ </ScrollView>
338
+ )
307
339
  }
308
- }, [backgroundImage, backgroundImageWidth, backgroundImageHeight, src])
340
+ return (
341
+ <View {...props} style={containerStyle} testID={testID} dataSet={dataSetValue} ref={ref}>
342
+ {content}
343
+ </View>
344
+ )
345
+ }
309
346
 
310
347
  const dataSetValue = boxMediaIds ? { media: boxMediaIds, ...dataSet } : dataSet
311
348
 
@@ -416,10 +453,12 @@ Box.propTypes = {
416
453
  */
417
454
  customGradient: PropTypes.func,
418
455
  /**
419
- * Use this prop to add a background image to the box.
456
+ * Apply background image to the box.
420
457
  */
421
458
  backgroundImage: PropTypes.shape({
422
- src: PropTypes.string.isRequired,
459
+ // The image src is either a URI string or a number (when a local image src is bundled in IOS or Android app)
460
+ // src is an object when used responsively to provide different image sources for different screen sizes
461
+ src: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object]).isRequired,
423
462
  alt: PropTypes.string,
424
463
  resizeMode: responsiveProps.getTypeOptionallyByViewport(
425
464
  PropTypes.oneOf(['cover', 'contain', 'stretch', 'repeat', 'center'])
@@ -436,18 +475,21 @@ Box.propTypes = {
436
475
  export default Box
437
476
 
438
477
  const staticStyles = StyleSheet.create({
439
- backgroundImageContainer: {
440
- flex: 1
441
- },
442
- containedContainer: {
443
- flex: 1,
444
- overflow: 'hidden'
478
+ imageBackground: { width: '100%', height: '100%' },
479
+ contentOverlay: {
480
+ position: 'relative',
481
+ width: '100%',
482
+ height: '100%',
483
+ zIndex: 1
445
484
  },
446
- containedView: {
447
- zIndex: -1,
448
- position: 'absolute'
485
+ containContainer: {
486
+ width: '100%',
487
+ height: '100%',
488
+ overflow: 'hidden',
489
+ position: 'relative'
449
490
  },
450
- containedImage: {
491
+ containImage: {
492
+ position: 'absolute',
451
493
  width: '100%',
452
494
  height: '100%'
453
495
  }
@@ -1,21 +1,54 @@
1
- export default {
2
- 'top-start': { top: 0 },
3
- 'top-center': { left: 0, right: 0, marginHorizontal: 'auto' },
1
+ import { Platform } from 'react-native'
2
+
3
+ const webStyles = {
4
+ 'top-start': { top: 0, left: 0 },
5
+ 'top-center': { top: 0, left: '50%', transform: [{ translateX: '-50%' }] },
4
6
  'top-end': { top: 0, right: 0 },
5
7
  'right-start': { top: 0, right: 0 },
6
- 'left-start': { top: 0 },
7
- 'left-center': { top: 0, bottom: 0, marginVertical: 'auto' },
8
- 'none-start': { top: 0, bottom: 0, marginVertical: 'auto' },
9
- 'none-center': { top: 0, bottom: 0, left: 0, right: 0, margin: 'auto' },
10
- 'right-center': { top: 0, bottom: 0, right: 0, marginVertical: 'auto' },
11
- 'none-end': { top: 0, bottom: 0, right: 0, marginVertical: 'auto' },
8
+ 'left-start': { top: 0, left: 0 },
9
+ 'left-center': { top: '50%', left: 0, transform: [{ translateY: '-50%' }] },
10
+ 'right-center': { top: '50%', right: 0, transform: [{ translateY: '-50%' }] },
12
11
  'bottom-start': { bottom: 0, left: 0 },
13
12
  'left-end': { bottom: 0, left: 0 },
14
- 'bottom-center': { left: 0, right: 0, bottom: 0, marginHorizontal: 'auto' },
15
- 'bottom-end': { right: 0, bottom: 0 },
16
- 'right-end': { right: 0, bottom: 0 },
17
- 'top-stretch': { left: 0, right: 0, width: '100%' },
18
- 'left-stretch': { top: 0, bottom: 0, height: '100%' },
13
+ 'bottom-center': { bottom: 0, left: '50%', transform: [{ translateX: '-50%' }] },
14
+ 'bottom-end': { bottom: 0, right: 0 },
15
+ 'right-end': { bottom: 0, right: 0 },
16
+ 'top-stretch': { top: 0, left: 0, right: 0, width: '100%' },
17
+ 'left-stretch': { top: 0, bottom: 0, left: 0, height: '100%' },
19
18
  'right-stretch': { top: 0, bottom: 0, right: 0, height: '100%' },
20
- 'bottom-stretch': { left: 0, right: 0, bottom: 0, width: '100%' }
19
+ 'bottom-stretch': { bottom: 0, left: 0, right: 0, width: '100%' }
21
20
  }
21
+
22
+ const webBackgroundPositions = {
23
+ 'top-start': 'left top',
24
+ 'top-center': 'center top',
25
+ 'top-end': 'right top',
26
+ 'bottom-start': 'left bottom',
27
+ 'bottom-center': 'center bottom',
28
+ 'bottom-end': 'right bottom',
29
+ 'left-center': 'left center',
30
+ 'right-center': 'right center'
31
+ }
32
+
33
+ const nativeStyles = {
34
+ 'top-start': { top: 0, left: 0, width: 150, height: 200 },
35
+ 'top-center': { top: 0, left: '50%', marginLeft: -75, width: 150, height: 200 },
36
+ 'top-end': { top: 0, right: 0, width: 150, height: 200 },
37
+ 'right-start': { top: 0, right: 0, width: 150, height: 200 },
38
+ 'left-start': { top: 0, left: 0, width: 150, height: 200 },
39
+ 'left-center': { left: 0, top: '50%', marginTop: -100, width: 150, height: 200 },
40
+ 'right-center': { right: 0, top: '50%', marginTop: -100, width: 150, height: 200 },
41
+ 'bottom-start': { bottom: 0, left: 0, width: 150, height: 200 },
42
+ 'left-end': { bottom: 0, left: 0, width: 150, height: 200 },
43
+ 'bottom-center': { bottom: 0, left: '50%', marginLeft: -75, width: 150, height: 200 },
44
+ 'bottom-end': { bottom: 0, right: 0, width: 150, height: 200 },
45
+ 'right-end': { bottom: 0, right: 0, width: 150, height: 200 },
46
+ 'top-stretch': { top: 0, left: 0, right: 0, width: '100%' },
47
+ 'left-stretch': { top: 0, bottom: 0, left: 0, height: '100%' },
48
+ 'right-stretch': { top: 0, bottom: 0, right: 0, height: '100%' },
49
+ 'bottom-stretch': { bottom: 0, left: 0, right: 0, width: '100%' }
50
+ }
51
+
52
+ export const backgroundPositions = Platform.OS === 'web' ? webBackgroundPositions : {}
53
+
54
+ export default Platform.OS === 'web' ? webStyles : nativeStyles
@@ -12,7 +12,8 @@ import {
12
12
  a11yProps,
13
13
  viewProps,
14
14
  useCopy,
15
- unpackFragment
15
+ unpackFragment,
16
+ isTouchDevice
16
17
  } from '../utils'
17
18
  import { useA11yInfo } from '../A11yInfoProvider'
18
19
  import { CarouselProvider } from './CarouselContext'
@@ -799,7 +800,7 @@ const Carousel = React.forwardRef(
799
800
  return false
800
801
  }
801
802
  if (Platform.OS === 'web') {
802
- return !!(viewport === 'xs' || viewport === 'sm')
803
+ return !!(viewport === 'xs' || viewport === 'sm' || (viewport === 'md' && isTouchDevice()))
803
804
  }
804
805
  return true
805
806
  }, [viewport, totalItems])
@@ -118,6 +118,7 @@ const Modal = React.forwardRef(
118
118
  confirmButtonVariant,
119
119
  cancelButtonText,
120
120
  cancelButtonType,
121
+ footer,
121
122
  ...rest
122
123
  },
123
124
  ref
@@ -240,6 +241,7 @@ const Modal = React.forwardRef(
240
241
  confirmButtonVariant={confirmButtonVariant}
241
242
  cancelButtonText={cancelButtonText}
242
243
  cancelButtonType={cancelButtonType}
244
+ footer={footer}
243
245
  >
244
246
  {Platform.OS !== 'web' ? (
245
247
  <ScrollView style={selectScrollViewStyles}>{children}</ScrollView>
@@ -343,7 +345,11 @@ Modal.propTypes = {
343
345
  /**
344
346
  * Receive a function for the onCancel event in the cancel button.
345
347
  */
346
- onCancel: PropTypes.func
348
+ onCancel: PropTypes.func,
349
+ /**
350
+ * Receive a react node or an array of nodes to render at the bottom of the modal, above the action buttons.
351
+ */
352
+ footer: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)])
347
353
  }
348
354
 
349
355
  export default Modal
@@ -25,7 +25,8 @@ const ModalContent = React.forwardRef(
25
25
  cancelButtonText,
26
26
  cancelButtonType: CancelButton = TextButton,
27
27
  children,
28
- onCancel
28
+ onCancel,
29
+ footer
29
30
  },
30
31
  ref
31
32
  ) => {
@@ -131,7 +132,7 @@ const ModalContent = React.forwardRef(
131
132
  </Box>
132
133
  )}
133
134
  {children}
134
- {(hasConfirmButton || hasCancelButton) && (
135
+ {(hasConfirmButton || hasCancelButton) && !footer && (
135
136
  <View
136
137
  style={[
137
138
  selectFooterContainerStyles({
@@ -158,6 +159,7 @@ const ModalContent = React.forwardRef(
158
159
  ) : null}
159
160
  </View>
160
161
  )}
162
+ {footer}
161
163
  </View>
162
164
  )
163
165
  }
@@ -190,7 +192,8 @@ ModalContent.propTypes = {
190
192
  cancelButtonText: PropTypes.string,
191
193
  cancelButtonType: PropTypes.elementType, // TODO: figure out a way of passing an icon to the TextButton
192
194
  children: PropTypes.node,
193
- onCancel: PropTypes.func
195
+ onCancel: PropTypes.func,
196
+ footer: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)])
194
197
  }
195
198
 
196
199
  export default ModalContent
@@ -479,9 +479,9 @@ MultiSelectFilter.propTypes = {
479
479
  */
480
480
  label: PropTypes.string.isRequired,
481
481
  /**
482
- * The text for the subtitle
482
+ * The text for the subtitle. Can also be JSX.
483
483
  */
484
- subtitle: PropTypes.string,
484
+ subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
485
485
  /**
486
486
  * An optional unique string may be provided to identify the ButtonDropdown.
487
487
  * If not provided, the label is used.