@telus-uds/components-base 4.0.0-alpha.2 → 4.0.0-beta.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 (120) hide show
  1. package/CHANGELOG.md +47 -1
  2. package/lib/cjs/Autocomplete/Autocomplete.js +13 -1
  3. package/lib/cjs/Autocomplete/constants.js +1 -1
  4. package/lib/cjs/Button/ButtonGroup.js +91 -23
  5. package/lib/cjs/Card/CardBase.js +75 -47
  6. package/lib/cjs/ColourToggle/ColourBubble.js +66 -13
  7. package/lib/cjs/ColourToggle/ColourToggle.js +41 -11
  8. package/lib/cjs/ColourToggle/constants.js +15 -0
  9. package/lib/cjs/ColourToggle/dictionary.js +14 -0
  10. package/lib/cjs/Listbox/ListboxOverlay.js +7 -1
  11. package/lib/cjs/MultiSelectFilter/ModalOverlay.js +7 -1
  12. package/lib/cjs/MultiSelectFilter/MultiSelectFilter.js +2 -28
  13. package/lib/cjs/TextInput/TextInputBase.js +2 -1
  14. package/lib/cjs/Validator/Validator.js +5 -1
  15. package/lib/cjs/utils/useOverlaidPosition.js +83 -42
  16. package/lib/esm/Autocomplete/Autocomplete.js +13 -1
  17. package/lib/esm/Autocomplete/constants.js +1 -1
  18. package/lib/esm/Button/ButtonGroup.js +91 -23
  19. package/lib/esm/Card/CardBase.js +75 -47
  20. package/lib/esm/ColourToggle/ColourBubble.js +67 -14
  21. package/lib/esm/ColourToggle/ColourToggle.js +41 -11
  22. package/lib/esm/ColourToggle/constants.js +8 -0
  23. package/lib/esm/ColourToggle/dictionary.js +8 -0
  24. package/lib/esm/Listbox/ListboxOverlay.js +7 -1
  25. package/lib/esm/MultiSelectFilter/ModalOverlay.js +8 -2
  26. package/lib/esm/MultiSelectFilter/MultiSelectFilter.js +2 -28
  27. package/lib/esm/TextInput/TextInputBase.js +2 -1
  28. package/lib/esm/Validator/Validator.js +5 -1
  29. package/lib/esm/utils/useOverlaidPosition.js +83 -42
  30. package/lib/package.json +3 -3
  31. package/package.json +3 -3
  32. package/src/Autocomplete/Autocomplete.jsx +11 -2
  33. package/src/Autocomplete/constants.js +1 -1
  34. package/src/Button/ButtonGroup.jsx +93 -24
  35. package/src/Card/CardBase.jsx +94 -75
  36. package/src/ColourToggle/ColourBubble.jsx +62 -7
  37. package/src/ColourToggle/ColourToggle.jsx +43 -9
  38. package/src/ColourToggle/constants.js +10 -0
  39. package/src/ColourToggle/dictionary.js +4 -0
  40. package/src/Listbox/ListboxOverlay.jsx +6 -2
  41. package/src/MultiSelectFilter/ModalOverlay.jsx +9 -3
  42. package/src/MultiSelectFilter/MultiSelectFilter.jsx +1 -31
  43. package/src/TextInput/TextInputBase.jsx +2 -1
  44. package/src/Validator/Validator.jsx +5 -1
  45. package/src/utils/useOverlaidPosition.js +84 -34
  46. package/types/A11yInfoProvider.d.ts +9 -0
  47. package/types/A11yText.d.ts +13 -0
  48. package/types/ActionCard.d.ts +19 -0
  49. package/types/ActivityIndicator.d.ts +13 -0
  50. package/types/Autocomplete.d.ts +27 -0
  51. package/types/Badge.d.ts +15 -4
  52. package/types/BaseProvider.d.ts +26 -0
  53. package/types/Box.d.ts +13 -3
  54. package/types/Button.d.ts +35 -0
  55. package/types/Card.d.ts +19 -0
  56. package/types/CardGroup.d.ts +23 -0
  57. package/types/Carousel.d.ts +25 -0
  58. package/types/Checkbox.d.ts +15 -2
  59. package/types/CheckboxCard.d.ts +20 -0
  60. package/types/CheckboxCardGroup.d.ts +10 -4
  61. package/types/ChevronLink.d.ts +9 -2
  62. package/types/ColourToggle.d.ts +16 -0
  63. package/types/Common.d.ts +7 -3
  64. package/types/Divider.d.ts +13 -1
  65. package/types/DownloadApp.d.ts +19 -0
  66. package/types/ExpandCollapse.d.ts +16 -9
  67. package/types/ExpandCollapseMini.d.ts +16 -0
  68. package/types/Feedback.d.ts +16 -0
  69. package/types/Fieldset.d.ts +17 -0
  70. package/types/FileUpload.d.ts +6 -0
  71. package/types/FlexGrid.d.ts +57 -0
  72. package/types/Footnote.d.ts +18 -0
  73. package/types/HorizontalScroll.d.ts +15 -0
  74. package/types/HorizontalScrollButton.d.ts +17 -1
  75. package/types/Icon.d.ts +13 -2
  76. package/types/IconButton.d.ts +19 -0
  77. package/types/InputLabel.d.ts +12 -0
  78. package/types/InputSupports.d.ts +19 -0
  79. package/types/Link.d.ts +20 -4
  80. package/types/List.d.ts +11 -4
  81. package/types/Listbox.d.ts +13 -3
  82. package/types/Modal.d.ts +25 -0
  83. package/types/MultiSelectFilter.d.ts +24 -0
  84. package/types/Notification.d.ts +19 -0
  85. package/types/OrderedList.d.ts +17 -0
  86. package/types/Pagination.d.ts +25 -0
  87. package/types/Portal.d.ts +5 -1
  88. package/types/PriceLockup.d.ts +28 -0
  89. package/types/ProductCard.d.ts +20 -0
  90. package/types/ProductCardGroup.d.ts +21 -0
  91. package/types/Progress.d.ts +22 -0
  92. package/types/QuickLinks.d.ts +27 -0
  93. package/types/QuickLinksFeature.d.ts +27 -0
  94. package/types/Radio.d.ts +22 -0
  95. package/types/RadioCard.d.ts +21 -0
  96. package/types/Responsive.d.ts +14 -0
  97. package/types/Scroll.d.ts +2 -2
  98. package/types/Search.d.ts +15 -2
  99. package/types/Select.d.ts +12 -4
  100. package/types/SideNav.d.ts +38 -0
  101. package/types/Skeleton.d.ts +17 -0
  102. package/types/SkipLink.d.ts +14 -0
  103. package/types/Spacer.d.ts +8 -1
  104. package/types/StackView.d.ts +8 -2
  105. package/types/Status.d.ts +2 -3
  106. package/types/StepTracker.d.ts +16 -0
  107. package/types/TabBar.d.ts +25 -0
  108. package/types/Tabs.d.ts +12 -3
  109. package/types/Tags.d.ts +24 -0
  110. package/types/TextButton.d.ts +11 -4
  111. package/types/TextInput.d.ts +38 -0
  112. package/types/ThemeProvider.d.ts +29 -0
  113. package/types/Timeline.d.ts +16 -0
  114. package/types/ToggleSwitch.d.ts +11 -2
  115. package/types/ToolTip.d.ts +7 -3
  116. package/types/TooltipButton.d.ts +20 -0
  117. package/types/Typography.d.ts +8 -2
  118. package/types/Validator.d.ts +16 -0
  119. package/types/ViewportProvider.d.ts +12 -0
  120. package/types/index.d.ts +197 -50
@@ -19,9 +19,7 @@ const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, vie
19
19
 
20
20
  const GRID_COLUMNS = 12
21
21
 
22
- const isOverlayColor = (color) => {
23
- return color && typeof color === 'string' && color.startsWith('rgba(')
24
- }
22
+ const isOverlayColor = (color) => color && typeof color === 'string' && color.startsWith('rgba(')
25
23
 
26
24
  const setBackgroundImage = ({
27
25
  src,
@@ -30,7 +28,8 @@ const setBackgroundImage = ({
30
28
  backgroundImagePosition,
31
29
  backgroundImageAlign,
32
30
  content,
33
- cardStyle
31
+ cardStyle,
32
+ testID
34
33
  }) => {
35
34
  const borderRadius = cardStyle?.borderRadius || 0
36
35
  const borderWidth = cardStyle?.borderWidth || 0
@@ -96,6 +95,7 @@ const setBackgroundImage = ({
96
95
  style={[staticStyles.imageBackground, backgroundImageStyle]}
97
96
  role="img"
98
97
  aria-label={alt}
98
+ testID={testID}
99
99
  >
100
100
  {content}
101
101
  </View>
@@ -113,6 +113,7 @@ const setBackgroundImage = ({
113
113
  accessible={true}
114
114
  accessibilityLabel={alt}
115
115
  accessibilityIgnoresInvertColors={true}
116
+ testID={testID}
116
117
  />
117
118
  <View style={staticStyles.contentOverlay}>{content}</View>
118
119
  </View>
@@ -127,33 +128,18 @@ const setBackgroundImage = ({
127
128
  style={staticStyles.imageBackground}
128
129
  accessible={true}
129
130
  accessibilityLabel={alt}
131
+ testID={testID}
130
132
  >
131
133
  {content}
132
134
  </ImageBackground>
133
135
  )
134
136
  }
135
137
 
136
- const selectPaddedContentStyles = ({
138
+ const selectPaddedContentStyles = ({ paddingTop, paddingBottom, paddingLeft, paddingRight }) => ({
137
139
  paddingTop,
138
140
  paddingBottom,
139
141
  paddingLeft,
140
- paddingRight,
141
- borderWidth,
142
- borderColor,
143
- borderRadius,
144
- hasInteractiveBorder
145
- }) => ({
146
- paddingTop,
147
- paddingBottom,
148
- paddingLeft,
149
- paddingRight,
150
- ...(hasInteractiveBorder
151
- ? {
152
- borderWidth,
153
- borderColor,
154
- borderRadius
155
- }
156
- : {})
142
+ paddingRight
157
143
  })
158
144
 
159
145
  const selectInteractiveOverlayStyles = ({ backgroundColor, borderRadius, borderWidth }) => {
@@ -167,32 +153,49 @@ const selectInteractiveOverlayStyles = ({ backgroundColor, borderRadius, borderW
167
153
  backgroundColor,
168
154
  borderRadius: adjustedBorderRadius,
169
155
  pointerEvents: 'none',
170
- zIndex: 1
156
+ zIndex: 3
171
157
  }
172
158
  }
173
159
 
174
- // Ensure explicit selection of tokens
175
- export const selectStyles = ({
176
- flex,
160
+ const selectOuterContainerStyles = ({
161
+ containerStyle,
177
162
  backgroundColor,
178
- borderColor,
179
163
  borderRadius,
164
+ borderColor,
180
165
  borderWidth,
181
- paddingBottom,
182
- paddingLeft,
183
- paddingRight,
184
- paddingTop,
185
- marginTop,
186
- marginBottom,
187
- marginLeft,
188
- marginRight,
189
- minWidth,
190
- shadow,
191
- backgroundGradient,
192
- gradient,
193
- maxHeight,
194
- overflowY
195
- }) => {
166
+ hasGradientToken
167
+ }) => ({
168
+ ...containerStyle,
169
+ backgroundColor,
170
+ borderRadius,
171
+ ...(hasGradientToken ? {} : { borderColor, borderWidth })
172
+ })
173
+
174
+ // Ensure explicit selection of tokens
175
+ export const selectStyles = (
176
+ {
177
+ flex,
178
+ backgroundColor,
179
+ borderColor,
180
+ borderRadius,
181
+ borderWidth,
182
+ paddingBottom,
183
+ paddingLeft,
184
+ paddingRight,
185
+ paddingTop,
186
+ marginTop,
187
+ marginBottom,
188
+ marginLeft,
189
+ marginRight,
190
+ minWidth,
191
+ shadow,
192
+ backgroundGradient,
193
+ gradient,
194
+ maxHeight,
195
+ overflowY
196
+ },
197
+ { hasBgImage = false } = {}
198
+ ) => {
196
199
  const hasGradient = (gradient || backgroundGradient) && Platform.OS === 'web'
197
200
 
198
201
  let backgroundImageValue = null
@@ -231,7 +234,9 @@ export const selectStyles = ({
231
234
  ? {
232
235
  backgroundImage: backgroundImageValue,
233
236
  backgroundOrigin: `border-box`,
234
- boxShadow: `inset 0 1000px ${boxShadowColor}`,
237
+ // bgImage child fills the content-box and replaces what the inset shadow
238
+ // used to cover, so we omit it to avoid hiding the image.
239
+ ...(hasBgImage ? {} : { boxShadow: `inset 0 1000px ${boxShadowColor}` }),
235
240
  border: `${borderWidth}px solid transparent`
236
241
  }
237
242
  : {}),
@@ -249,14 +254,13 @@ export const selectStyles = ({
249
254
  * intended to be used in apps or sites directly: build themed components on top of this.
250
255
  */
251
256
  export const CardBase = React.forwardRef(
252
- ({ children, tokens, dataSet, backgroundImage, fullBleedContent, cardState, ...rest }, ref) => {
257
+ (
258
+ { children, tokens, dataSet, backgroundImage, fullBleedContent, cardState, testID, ...rest },
259
+ ref
260
+ ) => {
253
261
  const resolvedTokens = typeof tokens === 'function' ? tokens(cardState) : tokens
254
- const tokensToUse =
255
- backgroundImage && backgroundImage.src
256
- ? { ...resolvedTokens, gradient: undefined, backgroundGradient: undefined }
257
- : resolvedTokens
258
-
259
- const cardStyle = selectStyles(tokensToUse)
262
+ const hasBgImage = Boolean(backgroundImage && backgroundImage.src)
263
+ const cardStyle = selectStyles(resolvedTokens, { hasBgImage })
260
264
  const props = selectProps(rest)
261
265
 
262
266
  let content = children
@@ -288,31 +292,28 @@ export const CardBase = React.forwardRef(
288
292
  } = cardStyle
289
293
 
290
294
  const hasPadding = paddingTop || paddingBottom || paddingLeft || paddingRight
291
- const hasInteractiveBorder = borderWidth && borderWidth > 0
292
295
  const hasInteractiveOverlay = isOverlayColor(backgroundColor)
296
+ const outerBackgroundColor = hasInteractiveOverlay ? undefined : backgroundColor
297
+ const hasGradientToken = Boolean(resolvedTokens.gradient) && Platform.OS === 'web'
293
298
 
294
- const paddedContent =
295
- hasPadding || hasInteractiveBorder ? (
296
- <View
297
- style={selectPaddedContentStyles({
298
- paddingTop,
299
- paddingBottom,
300
- paddingLeft,
301
- paddingRight,
302
- borderWidth,
303
- borderColor,
304
- borderRadius,
305
- hasInteractiveBorder
306
- })}
307
- >
308
- {children}
309
- </View>
310
- ) : (
311
- children
312
- )
299
+ const paddedContent = hasPadding ? (
300
+ <View
301
+ style={selectPaddedContentStyles({
302
+ paddingTop,
303
+ paddingBottom,
304
+ paddingLeft,
305
+ paddingRight
306
+ })}
307
+ >
308
+ {children}
309
+ </View>
310
+ ) : (
311
+ children
312
+ )
313
313
 
314
314
  const contentWithOverlay = (
315
315
  <>
316
+ <View style={staticStyles.contentOverlay}>{paddedContent}</View>
316
317
  {hasInteractiveOverlay && Platform.OS === 'web' && (
317
318
  <View
318
319
  style={selectInteractiveOverlayStyles({
@@ -320,9 +321,9 @@ export const CardBase = React.forwardRef(
320
321
  borderRadius,
321
322
  borderWidth
322
323
  })}
324
+ testID={testID && `${testID}-card-base-bg-overlay`}
323
325
  />
324
326
  )}
325
- <View style={staticStyles.contentOverlay}>{paddedContent}</View>
326
327
  </>
327
328
  )
328
329
 
@@ -333,11 +334,25 @@ export const CardBase = React.forwardRef(
333
334
  backgroundImagePosition,
334
335
  backgroundImageAlign,
335
336
  content: contentWithOverlay,
336
- cardStyle: { ...containerStyle, borderRadius }
337
+ cardStyle: { borderRadius, borderWidth },
338
+ testID: testID && `${testID}-card-base-bg-image`
337
339
  })
338
340
 
339
341
  return (
340
- <View style={{ ...containerStyle, borderRadius }} dataSet={dataSet} ref={ref} {...props}>
342
+ <View
343
+ style={selectOuterContainerStyles({
344
+ containerStyle,
345
+ backgroundColor: outerBackgroundColor,
346
+ borderRadius,
347
+ borderColor,
348
+ borderWidth,
349
+ hasGradientToken
350
+ })}
351
+ dataSet={dataSet}
352
+ ref={ref}
353
+ testID={testID}
354
+ {...props}
355
+ >
341
356
  {content}
342
357
  </View>
343
358
  )
@@ -370,7 +385,7 @@ export const CardBase = React.forwardRef(
370
385
  )
371
386
 
372
387
  return (
373
- <View style={containerStyle} dataSet={dataSet} ref={ref} {...props}>
388
+ <View style={containerStyle} dataSet={dataSet} ref={ref} testID={testID} {...props}>
374
389
  <FlexGrid>
375
390
  <FlexGridRow>
376
391
  {imageFirst ? (
@@ -391,7 +406,7 @@ export const CardBase = React.forwardRef(
391
406
  }
392
407
 
393
408
  return (
394
- <View style={cardStyle} dataSet={dataSet} ref={ref} {...props}>
409
+ <View style={cardStyle} dataSet={dataSet} ref={ref} testID={testID} {...props}>
395
410
  {content}
396
411
  </View>
397
412
  )
@@ -439,6 +454,10 @@ CardBase.propTypes = {
439
454
  ...selectedSystemPropTypes,
440
455
  children: PropTypes.node,
441
456
  tokens: getTokensPropType('Card'),
457
+ /**
458
+ * Identifier for testing purposes.
459
+ */
460
+ testID: PropTypes.string,
442
461
  /**
443
462
  * Apply background image to the card.
444
463
  */
@@ -1,11 +1,12 @@
1
1
  import React from 'react'
2
2
  import PropTypes from 'prop-types'
3
3
 
4
- import { View, Pressable, Platform } from 'react-native'
4
+ import { View, Pressable, Platform, StyleSheet } from 'react-native'
5
5
  import { resolvePressableTokens } from '../utils/pressability'
6
6
  import { applyShadowToken } from '../ThemeProvider/utils/styles'
7
7
  import { getTokensPropType } from '../utils/props/tokens'
8
8
  import { Tooltip } from '../Tooltip/Tooltip.native'
9
+ import { UNAVAILABLE_VARIANT } from './constants'
9
10
 
10
11
  const selectGeneralBubbleTokens = ({
11
12
  outerBubbleHeight,
@@ -52,13 +53,41 @@ const selectBorderBubbleTokens = ({
52
53
  borderRadius: bubbleBorderRadius
53
54
  })
54
55
 
56
+ const selectOverlayStyles = ({ overlayColor, innerBubbleBorderRadius }) => ({
57
+ borderRadius: innerBubbleBorderRadius,
58
+ backgroundColor: overlayColor
59
+ })
60
+
61
+ const selectSlashStyles = (
62
+ { slashLightColor, slashDarkColor, slashWidth, slashOffset },
63
+ unavailable
64
+ ) => ({
65
+ width: slashWidth,
66
+ backgroundColor: unavailable === UNAVAILABLE_VARIANT.DARK ? slashDarkColor : slashLightColor,
67
+ marginLeft: slashOffset
68
+ })
69
+
55
70
  export const ColourBubble = React.forwardRef(
56
- ({ tokens = {}, id, colourHexCode, colourName, isSelected, onPress, showTooltip }, ref) => {
71
+ (
72
+ {
73
+ tokens = () => ({}),
74
+ id,
75
+ colourHexCode,
76
+ colourName,
77
+ isSelected,
78
+ onPress,
79
+ showTooltip,
80
+ unavailable,
81
+ isDisabled
82
+ },
83
+ ref
84
+ ) => {
57
85
  const defaultTokens = tokens({ selected: isSelected })
58
86
 
59
- const resolveColourBubbleTokens = (pressState) => resolvePressableTokens(tokens, pressState, {})
87
+ const resolveColourBubbleTokens = (pressState) =>
88
+ resolvePressableTokens(tokens, isDisabled ? {} : pressState, {})
60
89
 
61
- const themeTokens = React.useMemo(() => tokens(), [tokens])
90
+ const themeTokens = tokens()
62
91
 
63
92
  const pressable = (
64
93
  <Pressable
@@ -67,14 +96,23 @@ export const ColourBubble = React.forwardRef(
67
96
  isSelected && selectBorderBubbleTokens(defaultTokens)
68
97
  ]}
69
98
  onPress={onPress}
99
+ disabled={isDisabled}
100
+ focusable={!isDisabled}
70
101
  accessible
71
102
  accessibilityRole="radio"
72
103
  accessibilityLabel={colourName}
73
- accessibilityState={{ checked: isSelected }}
104
+ accessibilityState={{ checked: isSelected, disabled: isDisabled }}
74
105
  ref={ref}
75
106
  testID={id}
76
107
  >
77
- <View style={[selectInnerBubbleTokens(themeTokens), { backgroundColor: colourHexCode }]} />
108
+ <View style={[selectInnerBubbleTokens(themeTokens), { backgroundColor: colourHexCode }]}>
109
+ {unavailable && (
110
+ <View style={[StyleSheet.absoluteFillObject, selectOverlayStyles(themeTokens)]} />
111
+ )}
112
+ </View>
113
+ {unavailable && (
114
+ <View style={[staticStyles.indicator, selectSlashStyles(themeTokens, unavailable)]} />
115
+ )}
78
116
  </Pressable>
79
117
  )
80
118
 
@@ -121,5 +159,22 @@ ColourBubble.propTypes = {
121
159
  /**
122
160
  * When true, wraps the bubble in a Tooltip that displays the colourName on hover (web only).
123
161
  */
124
- showTooltip: PropTypes.bool
162
+ showTooltip: PropTypes.bool,
163
+ /**
164
+ * When set, renders a diagonal indicator over the bubble. Accepts either 'dark' or 'light' to determine the colour of the indicator.
165
+ */
166
+ unavailable: PropTypes.oneOf(Object.values(UNAVAILABLE_VARIANT)),
167
+ /**
168
+ * When true, disables interaction on the bubble.
169
+ */
170
+ isDisabled: PropTypes.bool
125
171
  }
172
+
173
+ const staticStyles = StyleSheet.create({
174
+ indicator: {
175
+ position: 'absolute',
176
+ top: '50%',
177
+ height: 2,
178
+ transform: [{ rotate: '-45deg' }]
179
+ }
180
+ })
@@ -8,31 +8,50 @@ import { getTokensPropType } from '../utils/props/tokens'
8
8
  import { selectSystemProps } from '../utils/props/selectSystemProps'
9
9
  import { variantProp } from '../utils/props/variantProp'
10
10
  import { viewProps } from '../utils/props/viewProps'
11
+ import { useCopy } from '../utils/useCopy'
11
12
  import { StackWrap } from '../StackView/StackWrap'
12
13
  import { Typography } from '../Typography/Typography'
13
14
  import { ColourBubble } from './ColourBubble'
15
+ import { UNAVAILABLE_VARIANT, DICTIONARY_CONTENT_SHAPE } from './constants'
16
+ import { DEFAULT_COLOUR_TOGGLE_DICTIONARY } from './dictionary'
14
17
 
15
18
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
16
19
 
17
20
  export const ColourToggle = React.forwardRef(
18
- ({ tokens, variant, defaultColourId, items, onChange, showTooltips, ...rest }, ref) => {
21
+ (
22
+ {
23
+ tokens,
24
+ variant,
25
+ defaultColourId,
26
+ items,
27
+ onChange,
28
+ showTooltips,
29
+ copy = 'en',
30
+ dictionary = DEFAULT_COLOUR_TOGGLE_DICTIONARY,
31
+ ...rest
32
+ },
33
+ ref
34
+ ) => {
19
35
  const [currentColourId, setCurrentColourId] = React.useState(defaultColourId)
20
36
  const getTokens = useThemeTokensCallback('ColourToggle', tokens, variant)
21
-
37
+ const getCopy = useCopy({ dictionary, copy })
22
38
  const { space } = getTokens()
23
- const { colourName: currentColourName = '' } =
24
- items.find((item) => item.id === currentColourId) || ''
39
+ const currentItem = items.find(({ id }) => id === currentColourId)
25
40
 
26
41
  return (
27
42
  <View ref={ref} {...selectProps(rest)}>
28
- <Typography>{currentColourName}</Typography>
43
+ <Typography>
44
+ {currentItem?.unavailable
45
+ ? `${currentItem.colourName} ${getCopy('unavailable')}`
46
+ : currentItem?.colourName ?? ''}
47
+ </Typography>
29
48
  <StackWrap space={space} accessibilityRole="radiogroup">
30
- {items.map(({ id, colourHexCode, colourName }, index) => {
49
+ {items.map(({ id, colourHexCode, colourName, unavailable, disabled }, index) => {
31
50
  const colourBubbleId = id || `ColourBubble[${index}]`
32
51
 
33
52
  const handleChangeColour = (event) => {
34
53
  setCurrentColourId(id)
35
- onChange?.(event, { id, colourHexCode, colourName })
54
+ onChange?.(event, { id, colourHexCode, colourName, unavailable })
36
55
  }
37
56
 
38
57
  return (
@@ -45,6 +64,8 @@ export const ColourToggle = React.forwardRef(
45
64
  colourName={colourName}
46
65
  onPress={handleChangeColour}
47
66
  showTooltip={showTooltips}
67
+ unavailable={unavailable}
68
+ isDisabled={!!disabled}
48
69
  />
49
70
  )
50
71
  })}
@@ -76,7 +97,9 @@ ColourToggle.propTypes = {
76
97
  PropTypes.exact({
77
98
  colourHexCode: PropTypes.string,
78
99
  colourName: PropTypes.string,
79
- id: PropTypes.string
100
+ id: PropTypes.string,
101
+ unavailable: PropTypes.oneOf(Object.values(UNAVAILABLE_VARIANT)),
102
+ disabled: PropTypes.bool
80
103
  })
81
104
  ),
82
105
  /**
@@ -86,5 +109,16 @@ ColourToggle.propTypes = {
86
109
  /**
87
110
  * When true, displays each colour's name as a tooltip on hover (web only).
88
111
  */
89
- showTooltips: PropTypes.bool
112
+ showTooltips: PropTypes.bool,
113
+ /**
114
+ * Select English or French copy.
115
+ */
116
+ copy: PropTypes.oneOf(['en', 'fr']),
117
+ /**
118
+ * Override default labels.
119
+ */
120
+ dictionary: PropTypes.shape({
121
+ en: DICTIONARY_CONTENT_SHAPE,
122
+ fr: DICTIONARY_CONTENT_SHAPE
123
+ })
90
124
  }
@@ -0,0 +1,10 @@
1
+ import PropTypes from 'prop-types'
2
+
3
+ export const UNAVAILABLE_VARIANT = {
4
+ DARK: 'dark',
5
+ LIGHT: 'light'
6
+ }
7
+
8
+ export const DICTIONARY_CONTENT_SHAPE = PropTypes.shape({
9
+ unavailable: PropTypes.string
10
+ })
@@ -0,0 +1,4 @@
1
+ export const DEFAULT_COLOUR_TOGGLE_DICTIONARY = {
2
+ en: { unavailable: '(Out of stock)' },
3
+ fr: { unavailable: '(En rupture de stock)' }
4
+ }
@@ -1,6 +1,6 @@
1
1
  import React from 'react'
2
2
  import PropTypes from 'prop-types'
3
- import { View, StyleSheet, Platform } from 'react-native'
3
+ import { View, StyleSheet, Platform, ScrollView } from 'react-native'
4
4
  import { Portal } from '../Portal/Portal'
5
5
  import { useThemeTokens } from '../ThemeProvider/useThemeTokens'
6
6
  import { Card } from '../Card/Card'
@@ -69,7 +69,11 @@ const DropdownOverlay = React.forwardRef(
69
69
  paddingRight: paddingHorizontal
70
70
  }}
71
71
  >
72
- {children}
72
+ {Platform.OS !== 'web' ? (
73
+ <ScrollView style={{ maxHeight }}>{children}</ScrollView>
74
+ ) : (
75
+ children
76
+ )}
73
77
  </Card>
74
78
  </View>
75
79
  )
@@ -1,7 +1,8 @@
1
1
  import React from 'react'
2
2
  import PropTypes from 'prop-types'
3
3
  import { View, StyleSheet, Platform } from 'react-native'
4
- import { Portal } from '@gorhom/portal'
4
+ import { Portal as GorhomPortal } from '@gorhom/portal'
5
+ import { Portal as LocalPortal } from '../Portal/Portal'
5
6
  import { useCopy } from '../utils/useCopy'
6
7
  import { copyPropTypes } from '../utils/props/copyPropTypes'
7
8
  import { getTokensPropType } from '../utils/props/tokens'
@@ -107,8 +108,13 @@ export const ModalOverlay = React.forwardRef(
107
108
  const getCopy = useCopy({ dictionary, copy })
108
109
  const closeLabel = getCopy('closeButton')
109
110
 
111
+ // On web, use the local Portal (always appends to document.body) to ensure correct
112
+ // absolute positioning in MFE/iframe contexts. @gorhom/portal may render its host
113
+ // outside the iframe's document, causing coordinate mismatches when scrolled.
114
+ const PortalComponent = Platform.OS === 'web' ? LocalPortal : GorhomPortal
115
+
110
116
  return (
111
- <Portal>
117
+ <PortalComponent>
112
118
  <View
113
119
  ref={containerRef}
114
120
  onLayout={onLayout}
@@ -137,7 +143,7 @@ export const ModalOverlay = React.forwardRef(
137
143
  {children}
138
144
  </Card>
139
145
  </View>
140
- </Portal>
146
+ </PortalComponent>
141
147
  )
142
148
  }
143
149
  )
@@ -1,6 +1,5 @@
1
1
  import React from 'react'
2
2
  import PropTypes from 'prop-types'
3
- import { Portal } from '@gorhom/portal'
4
3
  import { View, StyleSheet, Dimensions, SafeAreaView, Platform, ScrollView } from 'react-native'
5
4
  import { useThemeTokens, useThemeTokensCallback } from '../ThemeProvider/useThemeTokens'
6
5
  import { applyTextStyles } from '../ThemeProvider/utils/styles'
@@ -218,27 +217,11 @@ export const MultiSelectFilter = React.forwardRef(
218
217
  onCancel()
219
218
  }
220
219
 
221
- const appRootRef = React.useRef(null)
222
- const [rootOffsets, setRootOffsets] = React.useState(null)
223
-
224
- React.useEffect(() => {
225
- if (rootOffsets) return
226
- appRootRef.current?.measureInWindow((x, y) => {
227
- // Only set offsets if they are positive
228
- // this is because we want to avoid negative offsets that could cause
229
- // the dropdown to be positioned incorrectly in some situations
230
- if (y > 0) setRootOffsets({ horizontal: x, vertical: y })
231
- })
232
- }, [isOpen, rootOffsets])
233
-
234
220
  const { align, offsets } = useResponsiveProp({
235
221
  xs: { align: { top: 'top', left: 'left' } },
236
222
  sm: {
237
223
  align: { top: 'bottom', left: 'left' },
238
- offsets: {
239
- vertical: 4 - (rootOffsets?.vertical || 0),
240
- horizontal: -rootOffsets?.horizontal || 0
241
- }
224
+ offsets: { vertical: 4 }
242
225
  }
243
226
  })
244
227
 
@@ -364,14 +347,6 @@ export const MultiSelectFilter = React.forwardRef(
364
347
 
365
348
  return (
366
349
  <>
367
- {/*
368
- This View is rendered inside a Portal to determine the application's root position.
369
- Capturing it is crucial to determine offsets to position the Modal correctly, preventing
370
- misalignment that can occur when the Portal does not render the Modal at the body level.
371
- */}
372
- <Portal>
373
- <View ref={appRootRef} style={styles.appRootRef} />
374
- </Portal>
375
350
  <ButtonDropdown
376
351
  ref={sourceRef}
377
352
  key={id}
@@ -460,11 +435,6 @@ const styles = StyleSheet.create({
460
435
  },
461
436
  scrollContainer: {
462
437
  padding: 1
463
- },
464
- appRootRef: {
465
- position: 'absolute',
466
- top: 0,
467
- left: 0
468
438
  }
469
439
  })
470
440
 
@@ -256,7 +256,8 @@ export const TextInputBase = React.forwardRef(
256
256
 
257
257
  const handleChangeText = (event) => {
258
258
  const text = event.nativeEvent?.text ?? event.target?.value
259
- let filteredText = isNumeric ? text?.replace(/[^\d]/g, '') || undefined : text
259
+ // Do NOT use `|| undefined`: empty string is falsy and would break the controlled contract
260
+ let filteredText = isNumeric ? text?.replace(/[^\d]/g, '') ?? '' : text
260
261
  if (type === 'card' && filteredText) {
261
262
  const formattedValue = filteredText.replace(/[^a-zA-Z0-9]/g, '')
262
263
  const regex = new RegExp(`([a-zA-Z0-9]{4})(?=[a-zA-Z0-9])`, 'g')
@@ -205,7 +205,11 @@ export const Validator = React.forwardRef(
205
205
 
206
206
  // Sync external value prop to internal state
207
207
  React.useEffect(() => {
208
- if (value && Number(value).toString() !== 'NaN') {
208
+ if (value === '') {
209
+ setCodes(Array(validatorsLength).fill(''))
210
+ return
211
+ }
212
+ if (Number(value).toString() !== 'NaN') {
209
213
  const digits = value.split('').slice(0, validatorsLength)
210
214
  const newCodes = Array(validatorsLength).fill('')
211
215
  digits.forEach((digit, i) => {