@telus-uds/components-base 3.30.0 → 3.32.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 (45) hide show
  1. package/CHANGELOG.md +31 -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 +65 -12
  7. package/lib/cjs/ColourToggle/ColourToggle.js +40 -11
  8. package/lib/cjs/ColourToggle/constants.js +15 -0
  9. package/lib/cjs/ColourToggle/dictionary.js +15 -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 +66 -13
  21. package/lib/esm/ColourToggle/ColourToggle.js +41 -12
  22. package/lib/esm/ColourToggle/constants.js +8 -0
  23. package/lib/esm/ColourToggle/dictionary.js +9 -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 +2 -2
  31. package/package.json +2 -2
  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 +50 -10
  38. package/src/ColourToggle/constants.js +10 -0
  39. package/src/ColourToggle/dictionary.js +6 -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
@@ -14,9 +14,7 @@ const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, vie
14
14
 
15
15
  const GRID_COLUMNS = 12
16
16
 
17
- const isOverlayColor = (color) => {
18
- return color && typeof color === 'string' && color.startsWith('rgba(')
19
- }
17
+ const isOverlayColor = (color) => color && typeof color === 'string' && color.startsWith('rgba(')
20
18
 
21
19
  const setBackgroundImage = ({
22
20
  src,
@@ -25,7 +23,8 @@ const setBackgroundImage = ({
25
23
  backgroundImagePosition,
26
24
  backgroundImageAlign,
27
25
  content,
28
- cardStyle
26
+ cardStyle,
27
+ testID
29
28
  }) => {
30
29
  const borderRadius = cardStyle?.borderRadius || 0
31
30
  const borderWidth = cardStyle?.borderWidth || 0
@@ -91,6 +90,7 @@ const setBackgroundImage = ({
91
90
  style={[staticStyles.imageBackground, backgroundImageStyle]}
92
91
  role="img"
93
92
  aria-label={alt}
93
+ testID={testID}
94
94
  >
95
95
  {content}
96
96
  </View>
@@ -108,6 +108,7 @@ const setBackgroundImage = ({
108
108
  accessible={true}
109
109
  accessibilityLabel={alt}
110
110
  accessibilityIgnoresInvertColors={true}
111
+ testID={testID}
111
112
  />
112
113
  <View style={staticStyles.contentOverlay}>{content}</View>
113
114
  </View>
@@ -122,33 +123,18 @@ const setBackgroundImage = ({
122
123
  style={staticStyles.imageBackground}
123
124
  accessible={true}
124
125
  accessibilityLabel={alt}
126
+ testID={testID}
125
127
  >
126
128
  {content}
127
129
  </ImageBackground>
128
130
  )
129
131
  }
130
132
 
131
- const selectPaddedContentStyles = ({
133
+ const selectPaddedContentStyles = ({ paddingTop, paddingBottom, paddingLeft, paddingRight }) => ({
132
134
  paddingTop,
133
135
  paddingBottom,
134
136
  paddingLeft,
135
- paddingRight,
136
- borderWidth,
137
- borderColor,
138
- borderRadius,
139
- hasInteractiveBorder
140
- }) => ({
141
- paddingTop,
142
- paddingBottom,
143
- paddingLeft,
144
- paddingRight,
145
- ...(hasInteractiveBorder
146
- ? {
147
- borderWidth,
148
- borderColor,
149
- borderRadius
150
- }
151
- : {})
137
+ paddingRight
152
138
  })
153
139
 
154
140
  const selectInteractiveOverlayStyles = ({ backgroundColor, borderRadius, borderWidth }) => {
@@ -162,32 +148,49 @@ const selectInteractiveOverlayStyles = ({ backgroundColor, borderRadius, borderW
162
148
  backgroundColor,
163
149
  borderRadius: adjustedBorderRadius,
164
150
  pointerEvents: 'none',
165
- zIndex: 1
151
+ zIndex: 3
166
152
  }
167
153
  }
168
154
 
169
- // Ensure explicit selection of tokens
170
- export const selectStyles = ({
171
- flex,
155
+ const selectOuterContainerStyles = ({
156
+ containerStyle,
172
157
  backgroundColor,
173
- borderColor,
174
158
  borderRadius,
159
+ borderColor,
175
160
  borderWidth,
176
- paddingBottom,
177
- paddingLeft,
178
- paddingRight,
179
- paddingTop,
180
- marginTop,
181
- marginBottom,
182
- marginLeft,
183
- marginRight,
184
- minWidth,
185
- shadow,
186
- backgroundGradient,
187
- gradient,
188
- maxHeight,
189
- overflowY
190
- }) => {
161
+ hasGradientToken
162
+ }) => ({
163
+ ...containerStyle,
164
+ backgroundColor,
165
+ borderRadius,
166
+ ...(hasGradientToken ? {} : { borderColor, borderWidth })
167
+ })
168
+
169
+ // Ensure explicit selection of tokens
170
+ export const selectStyles = (
171
+ {
172
+ flex,
173
+ backgroundColor,
174
+ borderColor,
175
+ borderRadius,
176
+ borderWidth,
177
+ paddingBottom,
178
+ paddingLeft,
179
+ paddingRight,
180
+ paddingTop,
181
+ marginTop,
182
+ marginBottom,
183
+ marginLeft,
184
+ marginRight,
185
+ minWidth,
186
+ shadow,
187
+ backgroundGradient,
188
+ gradient,
189
+ maxHeight,
190
+ overflowY
191
+ },
192
+ { hasBgImage = false } = {}
193
+ ) => {
191
194
  const hasGradient = (gradient || backgroundGradient) && Platform.OS === 'web'
192
195
 
193
196
  let backgroundImageValue = null
@@ -226,7 +229,9 @@ export const selectStyles = ({
226
229
  ? {
227
230
  backgroundImage: backgroundImageValue,
228
231
  backgroundOrigin: `border-box`,
229
- boxShadow: `inset 0 1000px ${boxShadowColor}`,
232
+ // bgImage child fills the content-box and replaces what the inset shadow
233
+ // used to cover, so we omit it to avoid hiding the image.
234
+ ...(hasBgImage ? {} : { boxShadow: `inset 0 1000px ${boxShadowColor}` }),
230
235
  border: `${borderWidth}px solid transparent`
231
236
  }
232
237
  : {}),
@@ -244,14 +249,13 @@ export const selectStyles = ({
244
249
  * intended to be used in apps or sites directly: build themed components on top of this.
245
250
  */
246
251
  const CardBase = React.forwardRef(
247
- ({ children, tokens, dataSet, backgroundImage, fullBleedContent, cardState, ...rest }, ref) => {
252
+ (
253
+ { children, tokens, dataSet, backgroundImage, fullBleedContent, cardState, testID, ...rest },
254
+ ref
255
+ ) => {
248
256
  const resolvedTokens = typeof tokens === 'function' ? tokens(cardState) : tokens
249
- const tokensToUse =
250
- backgroundImage && backgroundImage.src
251
- ? { ...resolvedTokens, gradient: undefined, backgroundGradient: undefined }
252
- : resolvedTokens
253
-
254
- const cardStyle = selectStyles(tokensToUse)
257
+ const hasBgImage = Boolean(backgroundImage && backgroundImage.src)
258
+ const cardStyle = selectStyles(resolvedTokens, { hasBgImage })
255
259
  const props = selectProps(rest)
256
260
 
257
261
  let content = children
@@ -283,31 +287,28 @@ const CardBase = React.forwardRef(
283
287
  } = cardStyle
284
288
 
285
289
  const hasPadding = paddingTop || paddingBottom || paddingLeft || paddingRight
286
- const hasInteractiveBorder = borderWidth && borderWidth > 0
287
290
  const hasInteractiveOverlay = isOverlayColor(backgroundColor)
291
+ const outerBackgroundColor = hasInteractiveOverlay ? undefined : backgroundColor
292
+ const hasGradientToken = Boolean(resolvedTokens.gradient) && Platform.OS === 'web'
288
293
 
289
- const paddedContent =
290
- hasPadding || hasInteractiveBorder ? (
291
- <View
292
- style={selectPaddedContentStyles({
293
- paddingTop,
294
- paddingBottom,
295
- paddingLeft,
296
- paddingRight,
297
- borderWidth,
298
- borderColor,
299
- borderRadius,
300
- hasInteractiveBorder
301
- })}
302
- >
303
- {children}
304
- </View>
305
- ) : (
306
- children
307
- )
294
+ const paddedContent = hasPadding ? (
295
+ <View
296
+ style={selectPaddedContentStyles({
297
+ paddingTop,
298
+ paddingBottom,
299
+ paddingLeft,
300
+ paddingRight
301
+ })}
302
+ >
303
+ {children}
304
+ </View>
305
+ ) : (
306
+ children
307
+ )
308
308
 
309
309
  const contentWithOverlay = (
310
310
  <>
311
+ <View style={staticStyles.contentOverlay}>{paddedContent}</View>
311
312
  {hasInteractiveOverlay && Platform.OS === 'web' && (
312
313
  <View
313
314
  style={selectInteractiveOverlayStyles({
@@ -315,9 +316,9 @@ const CardBase = React.forwardRef(
315
316
  borderRadius,
316
317
  borderWidth
317
318
  })}
319
+ testID={testID && `${testID}-card-base-bg-overlay`}
318
320
  />
319
321
  )}
320
- <View style={staticStyles.contentOverlay}>{paddedContent}</View>
321
322
  </>
322
323
  )
323
324
 
@@ -328,11 +329,25 @@ const CardBase = React.forwardRef(
328
329
  backgroundImagePosition,
329
330
  backgroundImageAlign,
330
331
  content: contentWithOverlay,
331
- cardStyle: { ...containerStyle, borderRadius }
332
+ cardStyle: { borderRadius, borderWidth },
333
+ testID: testID && `${testID}-card-base-bg-image`
332
334
  })
333
335
 
334
336
  return (
335
- <View style={{ ...containerStyle, borderRadius }} dataSet={dataSet} ref={ref} {...props}>
337
+ <View
338
+ style={selectOuterContainerStyles({
339
+ containerStyle,
340
+ backgroundColor: outerBackgroundColor,
341
+ borderRadius,
342
+ borderColor,
343
+ borderWidth,
344
+ hasGradientToken
345
+ })}
346
+ dataSet={dataSet}
347
+ ref={ref}
348
+ testID={testID}
349
+ {...props}
350
+ >
336
351
  {content}
337
352
  </View>
338
353
  )
@@ -365,7 +380,7 @@ const CardBase = React.forwardRef(
365
380
  )
366
381
 
367
382
  return (
368
- <View style={containerStyle} dataSet={dataSet} ref={ref} {...props}>
383
+ <View style={containerStyle} dataSet={dataSet} ref={ref} testID={testID} {...props}>
369
384
  <FlexGrid>
370
385
  <FlexGridRow>
371
386
  {imageFirst ? (
@@ -386,7 +401,7 @@ const CardBase = React.forwardRef(
386
401
  }
387
402
 
388
403
  return (
389
- <View style={cardStyle} dataSet={dataSet} ref={ref} {...props}>
404
+ <View style={cardStyle} dataSet={dataSet} ref={ref} testID={testID} {...props}>
390
405
  {content}
391
406
  </View>
392
407
  )
@@ -434,6 +449,10 @@ CardBase.propTypes = {
434
449
  ...selectedSystemPropTypes,
435
450
  children: PropTypes.node,
436
451
  tokens: getTokensPropType('Card'),
452
+ /**
453
+ * Identifier for testing purposes.
454
+ */
455
+ testID: PropTypes.string,
437
456
  /**
438
457
  * Apply background image to the card.
439
458
  */
@@ -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'
7
7
  import { getTokensPropType } from '../utils'
8
8
  import Tooltip from '../Tooltip'
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
  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 @@ 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,7 +159,24 @@ 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
  }
126
172
 
173
+ const staticStyles = StyleSheet.create({
174
+ indicator: {
175
+ position: 'absolute',
176
+ top: '50%',
177
+ height: 2,
178
+ transform: [{ rotate: '-45deg' }]
179
+ }
180
+ })
181
+
127
182
  export default ColourBubble
@@ -3,32 +3,57 @@ import { View } from 'react-native'
3
3
  import PropTypes from 'prop-types'
4
4
 
5
5
  import { useThemeTokensCallback } from '../ThemeProvider'
6
- import { a11yProps, getTokensPropType, selectSystemProps, variantProp, viewProps } from '../utils'
6
+ import {
7
+ a11yProps,
8
+ getTokensPropType,
9
+ selectSystemProps,
10
+ useCopy,
11
+ variantProp,
12
+ viewProps
13
+ } from '../utils'
7
14
  import { StackWrap } from '../StackView'
8
15
  import Typography from '../Typography'
9
16
  import ColourBubble from './ColourBubble'
17
+ import { UNAVAILABLE_VARIANT, DICTIONARY_CONTENT_SHAPE } from './constants'
18
+ import DEFAULT_COLOUR_TOGGLE_DICTIONARY from './dictionary'
10
19
 
11
20
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
12
21
 
13
22
  const ColourToggle = React.forwardRef(
14
- ({ tokens, variant, defaultColourId, items, onChange, showTooltips, ...rest }, ref) => {
23
+ (
24
+ {
25
+ tokens,
26
+ variant,
27
+ defaultColourId,
28
+ items,
29
+ onChange,
30
+ showTooltips,
31
+ copy = 'en',
32
+ dictionary = DEFAULT_COLOUR_TOGGLE_DICTIONARY,
33
+ ...rest
34
+ },
35
+ ref
36
+ ) => {
15
37
  const [currentColourId, setCurrentColourId] = React.useState(defaultColourId)
16
38
  const getTokens = useThemeTokensCallback('ColourToggle', tokens, variant)
17
-
39
+ const getCopy = useCopy({ dictionary, copy })
18
40
  const { space } = getTokens()
19
- const { colourName: currentColourName = '' } =
20
- items.find((item) => item.id === currentColourId) || ''
41
+ const currentItem = items.find(({ id }) => id === currentColourId)
21
42
 
22
43
  return (
23
44
  <View ref={ref} {...selectProps(rest)}>
24
- <Typography>{currentColourName}</Typography>
45
+ <Typography>
46
+ {currentItem?.unavailable
47
+ ? `${currentItem.colourName} ${getCopy('unavailable')}`
48
+ : currentItem?.colourName ?? ''}
49
+ </Typography>
25
50
  <StackWrap space={space} accessibilityRole="radiogroup">
26
- {items.map(({ id, colourHexCode, colourName }, index) => {
51
+ {items.map(({ id, colourHexCode, colourName, unavailable, disabled }, index) => {
27
52
  const colourBubbleId = id || `ColourBubble[${index}]`
28
53
 
29
54
  const handleChangeColour = (event) => {
30
55
  setCurrentColourId(id)
31
- onChange?.(event, { id, colourHexCode, colourName })
56
+ onChange?.(event, { id, colourHexCode, colourName, unavailable })
32
57
  }
33
58
 
34
59
  return (
@@ -41,6 +66,8 @@ const ColourToggle = React.forwardRef(
41
66
  colourName={colourName}
42
67
  onPress={handleChangeColour}
43
68
  showTooltip={showTooltips}
69
+ unavailable={unavailable}
70
+ isDisabled={!!disabled}
44
71
  />
45
72
  )
46
73
  })}
@@ -72,7 +99,9 @@ ColourToggle.propTypes = {
72
99
  PropTypes.exact({
73
100
  colourHexCode: PropTypes.string,
74
101
  colourName: PropTypes.string,
75
- id: PropTypes.string
102
+ id: PropTypes.string,
103
+ unavailable: PropTypes.oneOf(Object.values(UNAVAILABLE_VARIANT)),
104
+ disabled: PropTypes.bool
76
105
  })
77
106
  ),
78
107
  /**
@@ -82,7 +111,18 @@ ColourToggle.propTypes = {
82
111
  /**
83
112
  * When true, displays each colour's name as a tooltip on hover (web only).
84
113
  */
85
- showTooltips: PropTypes.bool
114
+ showTooltips: PropTypes.bool,
115
+ /**
116
+ * Select English or French copy.
117
+ */
118
+ copy: PropTypes.oneOf(['en', 'fr']),
119
+ /**
120
+ * Override default labels.
121
+ */
122
+ dictionary: PropTypes.shape({
123
+ en: DICTIONARY_CONTENT_SHAPE,
124
+ fr: DICTIONARY_CONTENT_SHAPE
125
+ })
86
126
  }
87
127
 
88
128
  export default ColourToggle
@@ -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,6 @@
1
+ const DEFAULT_COLOUR_TOGGLE_DICTIONARY = {
2
+ en: { unavailable: '(Out of stock)' },
3
+ fr: { unavailable: '(En rupture de stock)' }
4
+ }
5
+
6
+ export default DEFAULT_COLOUR_TOGGLE_DICTIONARY
@@ -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'
5
5
  import { useThemeTokens } from '../ThemeProvider'
6
6
  import Card from '../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 LocalPortal from '../Portal/Portal'
5
6
  import { useCopy, copyPropTypes, getTokensPropType, variantProp } from '../utils'
6
7
  import { useViewport } from '../ViewportProvider'
7
8
  import { useThemeTokens } from '../ThemeProvider'
@@ -105,8 +106,13 @@ const ModalOverlay = React.forwardRef(
105
106
  const getCopy = useCopy({ dictionary, copy })
106
107
  const closeLabel = getCopy('closeButton')
107
108
 
109
+ // On web, use the local Portal (always appends to document.body) to ensure correct
110
+ // absolute positioning in MFE/iframe contexts. @gorhom/portal may render its host
111
+ // outside the iframe's document, causing coordinate mismatches when scrolled.
112
+ const PortalComponent = Platform.OS === 'web' ? LocalPortal : GorhomPortal
113
+
108
114
  return (
109
- <Portal>
115
+ <PortalComponent>
110
116
  <View
111
117
  ref={containerRef}
112
118
  onLayout={onLayout}
@@ -135,7 +141,7 @@ const ModalOverlay = React.forwardRef(
135
141
  {children}
136
142
  </Card>
137
143
  </View>
138
- </Portal>
144
+ </PortalComponent>
139
145
  )
140
146
  }
141
147
  )
@@ -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, applyTextStyles } from '../ThemeProvider'
6
5
  import {
@@ -221,27 +220,11 @@ const MultiSelectFilter = React.forwardRef(
221
220
  onCancel()
222
221
  }
223
222
 
224
- const appRootRef = React.useRef(null)
225
- const [rootOffsets, setRootOffsets] = React.useState(null)
226
-
227
- React.useEffect(() => {
228
- if (rootOffsets) return
229
- appRootRef.current?.measureInWindow((x, y) => {
230
- // Only set offsets if they are positive
231
- // this is because we want to avoid negative offsets that could cause
232
- // the dropdown to be positioned incorrectly in some situations
233
- if (y > 0) setRootOffsets({ horizontal: x, vertical: y })
234
- })
235
- }, [isOpen, rootOffsets])
236
-
237
223
  const { align, offsets } = useResponsiveProp({
238
224
  xs: { align: { top: 'top', left: 'left' } },
239
225
  sm: {
240
226
  align: { top: 'bottom', left: 'left' },
241
- offsets: {
242
- vertical: 4 - (rootOffsets?.vertical || 0),
243
- horizontal: -rootOffsets?.horizontal || 0
244
- }
227
+ offsets: { vertical: 4 }
245
228
  }
246
229
  })
247
230
 
@@ -367,14 +350,6 @@ const MultiSelectFilter = React.forwardRef(
367
350
 
368
351
  return (
369
352
  <>
370
- {/*
371
- This View is rendered inside a Portal to determine the application's root position.
372
- Capturing it is crucial to determine offsets to position the Modal correctly, preventing
373
- misalignment that can occur when the Portal does not render the Modal at the body level.
374
- */}
375
- <Portal>
376
- <View ref={appRootRef} style={styles.appRootRef} />
377
- </Portal>
378
353
  <ButtonDropdown
379
354
  ref={sourceRef}
380
355
  key={id}
@@ -464,11 +439,6 @@ const styles = StyleSheet.create({
464
439
  },
465
440
  scrollContainer: {
466
441
  padding: 1
467
- },
468
- appRootRef: {
469
- position: 'absolute',
470
- top: 0,
471
- left: 0
472
442
  }
473
443
  })
474
444
 
@@ -256,7 +256,8 @@ 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')
@@ -204,7 +204,11 @@ const Validator = React.forwardRef(
204
204
 
205
205
  // Sync external value prop to internal state
206
206
  React.useEffect(() => {
207
- if (value && Number(value).toString() !== 'NaN') {
207
+ if (value === '') {
208
+ setCodes(Array(validatorsLength).fill(''))
209
+ return
210
+ }
211
+ if (Number(value).toString() !== 'NaN') {
208
212
  const digits = value.split('').slice(0, validatorsLength)
209
213
  const newCodes = Array(validatorsLength).fill('')
210
214
  digits.forEach((digit, i) => {