@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
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
3
3
  import View from "react-native-web/dist/exports/View";
4
4
  import StyleSheet from "react-native-web/dist/exports/StyleSheet";
5
5
  import Platform from "react-native-web/dist/exports/Platform";
6
+ import ScrollView from "react-native-web/dist/exports/ScrollView";
6
7
  import { Portal } from '../Portal/Portal';
7
8
  import { useThemeTokens } from '../ThemeProvider/useThemeTokens';
8
9
  import { Card } from '../Card/Card';
@@ -60,7 +61,12 @@ const DropdownOverlay = /*#__PURE__*/React.forwardRef((_ref, ref) => {
60
61
  paddingLeft: paddingHorizontal,
61
62
  paddingRight: paddingHorizontal
62
63
  },
63
- children: children
64
+ children: Platform.OS !== 'web' ? /*#__PURE__*/_jsx(ScrollView, {
65
+ style: {
66
+ maxHeight
67
+ },
68
+ children: children
69
+ }) : children
64
70
  })
65
71
  });
66
72
  });
@@ -3,7 +3,8 @@ import PropTypes from 'prop-types';
3
3
  import View from "react-native-web/dist/exports/View";
4
4
  import StyleSheet from "react-native-web/dist/exports/StyleSheet";
5
5
  import Platform from "react-native-web/dist/exports/Platform";
6
- import { Portal } from '@gorhom/portal';
6
+ import { Portal as GorhomPortal } from '@gorhom/portal';
7
+ import { Portal as LocalPortal } from '../Portal/Portal';
7
8
  import { useCopy } from '../utils/useCopy';
8
9
  import { copyPropTypes } from '../utils/props/copyPropTypes';
9
10
  import { getTokensPropType } from '../utils/props/tokens';
@@ -118,7 +119,12 @@ export const ModalOverlay = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
118
119
  copy
119
120
  });
120
121
  const closeLabel = getCopy('closeButton');
121
- return /*#__PURE__*/_jsx(Portal, {
122
+
123
+ // On web, use the local Portal (always appends to document.body) to ensure correct
124
+ // absolute positioning in MFE/iframe contexts. @gorhom/portal may render its host
125
+ // outside the iframe's document, causing coordinate mismatches when scrolled.
126
+ const PortalComponent = Platform.OS === 'web' ? LocalPortal : GorhomPortal;
127
+ return /*#__PURE__*/_jsx(PortalComponent, {
122
128
  children: /*#__PURE__*/_jsx(View, {
123
129
  ref: containerRef,
124
130
  onLayout: onLayout,
@@ -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 from "react-native-web/dist/exports/View";
5
4
  import StyleSheet from "react-native-web/dist/exports/StyleSheet";
6
5
  import Dimensions from "react-native-web/dist/exports/Dimensions";
@@ -218,20 +217,6 @@ export const MultiSelectFilter = /*#__PURE__*/React.forwardRef((_ref3, ref) => {
218
217
  setIsOpen(false);
219
218
  onCancel();
220
219
  };
221
- const appRootRef = React.useRef(null);
222
- const [rootOffsets, setRootOffsets] = React.useState(null);
223
- React.useEffect(() => {
224
- if (rootOffsets) return;
225
- appRootRef.current?.measureInWindow((x, y) => {
226
- // Only set offsets if they are positive
227
- // this is because we want to avoid negative offsets that could cause
228
- // the dropdown to be positioned incorrectly in some situations
229
- if (y > 0) setRootOffsets({
230
- horizontal: x,
231
- vertical: y
232
- });
233
- });
234
- }, [isOpen, rootOffsets]);
235
220
  const {
236
221
  align,
237
222
  offsets
@@ -248,8 +233,7 @@ export const MultiSelectFilter = /*#__PURE__*/React.forwardRef((_ref3, ref) => {
248
233
  left: 'left'
249
234
  },
250
235
  offsets: {
251
- vertical: 4 - (rootOffsets?.vertical || 0),
252
- horizontal: -rootOffsets?.horizontal || 0
236
+ vertical: 4
253
237
  }
254
238
  }
255
239
  });
@@ -386,12 +370,7 @@ export const MultiSelectFilter = /*#__PURE__*/React.forwardRef((_ref3, ref) => {
386
370
  })]
387
371
  });
388
372
  return /*#__PURE__*/_jsxs(_Fragment, {
389
- children: [/*#__PURE__*/_jsx(Portal, {
390
- children: /*#__PURE__*/_jsx(View, {
391
- ref: appRootRef,
392
- style: styles.appRootRef
393
- })
394
- }), /*#__PURE__*/_jsx(ButtonDropdown, {
373
+ children: [/*#__PURE__*/_jsx(ButtonDropdown, {
395
374
  ref: sourceRef,
396
375
  ...pressHandlers,
397
376
  value: isOpen,
@@ -471,11 +450,6 @@ const styles = StyleSheet.create({
471
450
  },
472
451
  scrollContainer: {
473
452
  padding: 1
474
- },
475
- appRootRef: {
476
- position: 'absolute',
477
- top: 0,
478
- left: 0
479
453
  }
480
454
  });
481
455
 
@@ -283,7 +283,8 @@ export const TextInputBase = /*#__PURE__*/React.forwardRef((_ref8, ref) => {
283
283
  }, [element, pattern]);
284
284
  const handleChangeText = event => {
285
285
  const text = event.nativeEvent?.text ?? event.target?.value;
286
- let filteredText = isNumeric ? text?.replace(/[^\d]/g, '') || undefined : text;
286
+ // Do NOT use `|| undefined`: empty string is falsy and would break the controlled contract
287
+ let filteredText = isNumeric ? text?.replace(/[^\d]/g, '') ?? '' : text;
287
288
  if (type === 'card' && filteredText) {
288
289
  const formattedValue = filteredText.replace(/[^a-zA-Z0-9]/g, '');
289
290
  const regex = new RegExp(`([a-zA-Z0-9]{4})(?=[a-zA-Z0-9])`, 'g');
@@ -200,7 +200,11 @@ export const Validator = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
200
200
 
201
201
  // Sync external value prop to internal state
202
202
  React.useEffect(() => {
203
- if (value && Number(value).toString() !== 'NaN') {
203
+ if (value === '') {
204
+ setCodes(Array(validatorsLength).fill(''));
205
+ return;
206
+ }
207
+ if (Number(value).toString() !== 'NaN') {
204
208
  const digits = value.split('').slice(0, validatorsLength);
205
209
  const newCodes = Array(validatorsLength).fill('');
206
210
  digits.forEach((digit, i) => {
@@ -54,46 +54,45 @@ function getOverlaidPosition(_ref2) {
54
54
  offsets = {},
55
55
  align
56
56
  } = _ref2;
57
- // Web-only: this will be difficult to mimic on native because there's no global scroll position.
58
- // TODO: wire something in e.g. a scroll ref accessible from a provider included in Allium provider
59
- // that can be passed to the appropriate ScrollView?
60
- const {
61
- scrollX = 0,
62
- scrollY = 0
63
- } = typeof window === 'object' ? window : {};
64
-
65
57
  // Will have top, bottom, left and/or right offsets depending on `align`
66
58
  const positioning = {};
67
59
  const verticalOffset = offsets.vertical ?? 0;
68
60
  const horizontalOffset = offsets.horizontal ?? 0;
69
61
  if (align.top) positioning.top = getPosition({
70
62
  edge: getEdgeType(align, 'top'),
71
- fromEdge: sourceLayout.y + scrollY + verticalOffset,
63
+ fromEdge: sourceLayout.y + verticalOffset,
72
64
  sourceSize: sourceLayout.height
73
65
  });
74
66
  if (align.middle) positioning.top = getPosition({
75
67
  edge: getEdgeType(align, 'middle'),
76
- fromEdge: sourceLayout.y + scrollY + verticalOffset - targetDimensions.height / 2,
77
- sourceSize: sourceLayout.height
78
- });
79
- if (align.bottom) positioning.bottom = getPosition({
80
- edge: getEdgeType(align, 'bottom'),
81
- fromEdge: windowDimensions.height - (sourceLayout.y + scrollY + sourceLayout.height - verticalOffset),
68
+ fromEdge: sourceLayout.y + verticalOffset - targetDimensions.height / 2,
82
69
  sourceSize: sourceLayout.height
83
70
  });
71
+ if (align.bottom) {
72
+ if (Platform.OS !== 'web') {
73
+ // On native, position:absolute is parent-relative, so use negative top offset instead of window-based bottom math.
74
+ positioning.top = sourceLayout.y - targetDimensions.height - verticalOffset;
75
+ } else {
76
+ positioning.bottom = getPosition({
77
+ edge: getEdgeType(align, 'bottom'),
78
+ fromEdge: windowDimensions.height - (sourceLayout.y + sourceLayout.height - verticalOffset),
79
+ sourceSize: sourceLayout.height
80
+ });
81
+ }
82
+ }
84
83
  if (align.left) positioning.left = getPosition({
85
84
  edge: getEdgeType(align, 'left'),
86
- fromEdge: sourceLayout.x + scrollX + horizontalOffset,
85
+ fromEdge: sourceLayout.x + horizontalOffset,
87
86
  sourceSize: sourceLayout.width
88
87
  });
89
88
  if (align.center) positioning.left = getPosition({
90
89
  edge: getEdgeType(align, 'center'),
91
- fromEdge: sourceLayout.x + scrollX + horizontalOffset - targetDimensions.width / 2,
90
+ fromEdge: sourceLayout.x + horizontalOffset - targetDimensions.width / 2,
92
91
  sourceSize: sourceLayout.width
93
92
  });
94
93
  if (align.right) positioning.right = getPosition({
95
94
  edge: getEdgeType(align, 'right'),
96
- fromEdge: windowDimensions.width - (sourceLayout.x + scrollX + sourceLayout.width - horizontalOffset),
95
+ fromEdge: windowDimensions.width - (sourceLayout.x + sourceLayout.width - horizontalOffset),
97
96
  sourceSize: sourceLayout.width
98
97
  });
99
98
  if (!(align.left && align.right)) {
@@ -134,6 +133,32 @@ export const useOverlaidPosition = _ref3 => {
134
133
  const targetRef = useRef(null);
135
134
  const [targetDimensions, setTargetDimensions] = useState(null);
136
135
  const [windowDimensions, setWindowDimensions] = useState(null);
136
+ const hasRemeasuredRef = useRef(false);
137
+ const applySourceLayout = useCallback((dims, x, y, width, height) => {
138
+ setWindowDimensions(dims);
139
+ setSourceLayout({
140
+ x,
141
+ y,
142
+ width,
143
+ height
144
+ });
145
+ }, []);
146
+ const measureSourceOnWeb = useCallback(windowDims => {
147
+ const el = sourceRef.current;
148
+ if (!el) return;
149
+ const domNode = el._nativeTag ?? el;
150
+ const dims = windowDims ?? Dimensions.get('window');
151
+ const rect = typeof domNode.getBoundingClientRect === 'function' ? domNode.getBoundingClientRect() : null;
152
+ if (rect) {
153
+ const x = rect.left + (typeof window.scrollX === 'number' ? window.scrollX : 0);
154
+ const y = rect.top + (typeof window.scrollY === 'number' ? window.scrollY : 0);
155
+ applySourceLayout(dims, x, y, rect.width, rect.height);
156
+ } else {
157
+ el.measureInWindow((mx, my, width, height) => {
158
+ applySourceLayout(dims, mx, my, width, height);
159
+ });
160
+ }
161
+ }, [applySourceLayout]);
137
162
  const onTargetLayout = useCallback(_ref4 => {
138
163
  let {
139
164
  nativeEvent: {
@@ -164,16 +189,19 @@ export const useOverlaidPosition = _ref3 => {
164
189
  let {
165
190
  window
166
191
  } = _ref5;
167
- const measurementFunction = Platform.OS === 'web' ? 'measureInWindow' : 'measure';
168
- sourceRef.current?.[measurementFunction]((x, y, width, height) => {
169
- setWindowDimensions(window);
170
- setSourceLayout({
171
- x,
172
- y,
173
- width,
174
- height
192
+ if (Platform.OS === 'web') {
193
+ measureSourceOnWeb(window);
194
+ } else {
195
+ sourceRef.current?.measure((x, y, width, height) => {
196
+ setWindowDimensions(window);
197
+ setSourceLayout({
198
+ x,
199
+ y,
200
+ width,
201
+ height
202
+ });
175
203
  });
176
- });
204
+ }
177
205
  };
178
206
  let subscription;
179
207
  const unsubscribe = () => {
@@ -186,6 +214,7 @@ export const useOverlaidPosition = _ref3 => {
186
214
  }
187
215
  setSourceLayout(null);
188
216
  setTargetDimensions(null);
217
+ hasRemeasuredRef.current = false;
189
218
  };
190
219
  if (readyToShow) {
191
220
  subscription = Dimensions.addEventListener('change', handleDimensionsChange);
@@ -196,36 +225,48 @@ export const useOverlaidPosition = _ref3 => {
196
225
  unsubscribe();
197
226
  }
198
227
  return unsubscribe;
199
- }, [readyToShow]);
228
+ }, [readyToShow, measureSourceOnWeb]);
229
+
230
+ // Re-measure source when targetDimensions first becomes available.
231
+ // Without this, there is a race condition: getBoundingClientRect() resolves faster than
232
+ // measureInWindow used to, so sourceLayout may be set before the dropdown has rendered
233
+ // and reported its dimensions via onTargetLayout. When targetDimensions finally arrives,
234
+ // sourceLayout is stale (no scroll has occurred to trigger handleScroll).
235
+ // Setting hasRemeasuredRef.current=true here also prevents the blink: isReady (on web)
236
+ // won't become true until this effect completes, guaranteeing the dropdown is always shown
237
+ // at its final correct position without an intermediate incorrect-position frame.
238
+ useEffect(() => {
239
+ if (!isShown || !sourceRef.current || !targetDimensions || Platform.OS !== 'web') return;
240
+ measureSourceOnWeb();
241
+ hasRemeasuredRef.current = true;
242
+ }, [targetDimensions, isShown, measureSourceOnWeb]);
200
243
  useEffect(() => {
201
244
  if (Platform.OS !== 'web') {
202
245
  return undefined;
203
246
  }
204
- const handleScroll = debounce(() => {
205
- sourceRef.current?.measureInWindow((x, y, width, height) => {
206
- setWindowDimensions(window);
207
- setSourceLayout({
208
- x,
209
- y,
210
- width,
211
- height
212
- });
213
- });
214
- }, DEBOUNCE_DELAY);
247
+ const handleScroll = debounce(measureSourceOnWeb, DEBOUNCE_DELAY);
215
248
  window.addEventListener('scroll', handleScroll);
216
249
  return () => {
217
250
  window.removeEventListener('scroll', handleScroll);
218
251
  handleScroll.cancel();
219
252
  };
220
- }, [sourceRef]);
221
- const isReady = Boolean(isShown && sourceLayout && windowDimensions && targetDimensions);
253
+ }, [measureSourceOnWeb]);
254
+
255
+ // On web, require hasRemeasuredRef to be true before declaring isReady. This ensures
256
+ // the dropdown is never shown with an intermediate stale position (the blink).
257
+ // On native, hasRemeasuredRef stays false (the web-only effect never runs), so we skip
258
+ // that check to preserve the original native behaviour.
259
+ const isReady = Boolean(isShown && sourceLayout && windowDimensions && targetDimensions && (Platform.OS !== 'web' || hasRemeasuredRef.current));
222
260
  const overlaidPosition = isReady ? getOverlaidPosition({
223
261
  sourceLayout,
224
262
  targetDimensions,
225
263
  windowDimensions,
226
264
  offsets,
227
265
  align
228
- }) : {};
266
+ }) : {
267
+ top: 0,
268
+ left: 0
269
+ };
229
270
  return {
230
271
  overlaidPosition,
231
272
  sourceRef,
package/lib/package.json CHANGED
@@ -11,8 +11,8 @@
11
11
  "@floating-ui/react-native": "^0.10.7",
12
12
  "@gorhom/portal": "^1.0.14",
13
13
  "@react-native-picker/picker": "^2.11.4",
14
- "@telus-uds/system-constants": "^4.0.0-alpha.0",
15
- "@telus-uds/system-theme-tokens": "^5.0.0-alpha.2",
14
+ "@telus-uds/system-constants": "^4.0.0-beta.0",
15
+ "@telus-uds/system-theme-tokens": "^5.0.0-beta.0",
16
16
  "css-mediaquery": "^0.1.2",
17
17
  "expo-document-picker": "^14.0.8",
18
18
  "expo-linear-gradient": "^15.0.8",
@@ -81,6 +81,6 @@
81
81
  "standard-engine": {
82
82
  "skip": true
83
83
  },
84
- "version": "4.0.0-alpha.2",
84
+ "version": "4.0.0-beta.1",
85
85
  "types": "types/index.d.ts"
86
86
  }
package/package.json CHANGED
@@ -11,8 +11,8 @@
11
11
  "@floating-ui/react-native": "^0.10.7",
12
12
  "@gorhom/portal": "^1.0.14",
13
13
  "@react-native-picker/picker": "^2.11.4",
14
- "@telus-uds/system-constants": "^4.0.0-alpha.0",
15
- "@telus-uds/system-theme-tokens": "^5.0.0-alpha.2",
14
+ "@telus-uds/system-constants": "^4.0.0-beta.0",
15
+ "@telus-uds/system-theme-tokens": "^5.0.0-beta.0",
16
16
  "css-mediaquery": "^0.1.2",
17
17
  "expo-document-picker": "^14.0.8",
18
18
  "expo-linear-gradient": "^15.0.8",
@@ -81,6 +81,6 @@
81
81
  "standard-engine": {
82
82
  "skip": true
83
83
  },
84
- "version": "4.0.0-alpha.2",
84
+ "version": "4.0.0-beta.1",
85
85
  "types": "types/index.d.ts"
86
86
  }
@@ -129,6 +129,7 @@ export const Autocomplete = React.forwardRef(
129
129
  value,
130
130
  helpText = '',
131
131
  loadingLabel,
132
+ dropdownPosition = 'bottom',
132
133
  tokens,
133
134
  ...rest
134
135
  },
@@ -180,7 +181,11 @@ export const Autocomplete = React.forwardRef(
180
181
  isShown: isExpanded || hintExpansionEnabled,
181
182
  offsets: {
182
183
  vertical: Platform.OS !== 'web' && (hint || inputLabel) ? 28 : 4
183
- }
184
+ },
185
+ align:
186
+ dropdownPosition === 'top'
187
+ ? { center: 'center', bottom: 'top' }
188
+ : { center: 'center', top: 'bottom' }
184
189
  })
185
190
  const targetRef = React.useRef(null)
186
191
  // We limit the number of suggestions displayed to avoid huge lists
@@ -554,5 +559,9 @@ Autocomplete.propTypes = {
554
559
  /**
555
560
  * Input value for controlled usage
556
561
  */
557
- value: PropTypes.string
562
+ value: PropTypes.string,
563
+ /**
564
+ * Controls whether the dropdown renders above or below the input. Defaults to 'bottom'.
565
+ */
566
+ dropdownPosition: PropTypes.oneOf(['top', 'bottom'])
558
567
  }
@@ -1,5 +1,5 @@
1
1
  export const DEFAULT_MIN_TO_SUGGESTION = 1
2
2
  export const DEFAULT_MAX_SUGGESTIONS = 5
3
- export const DEFAULT_MAX_DROPDOWN_HEIGHT = 336 // Approximately 7 items (48px each)
3
+ export const DEFAULT_MAX_DROPDOWN_HEIGHT = 500
4
4
  export const INPUT_LEFT_PADDING = 16
5
5
  export const MIN_LISTBOX_WIDTH = 288
@@ -25,6 +25,8 @@ const [selectItemProps, selectedItemPropTypes] = selectSystemProps([
25
25
  pressProps,
26
26
  viewProps
27
27
  ])
28
+ const EQUAL_WIDTH = 'equal'
29
+ const RESPONSIVE_WIDTH = 'responsive'
28
30
 
29
31
  export const ButtonGroup = React.forwardRef(
30
32
  (
@@ -67,6 +69,9 @@ export const ButtonGroup = React.forwardRef(
67
69
  const themeButtonTokensCallback = useThemeTokensCallback('ButtonGroupItem', tokens, variant)
68
70
  const gapValue = useSpacingScale(gap || space)
69
71
 
72
+ const isWeb = Platform.OS === 'web'
73
+ const buttonWidthVariant = variant?.width
74
+
70
75
  const getButtonTokens = useCallback(
71
76
  (state) => {
72
77
  const themeButtonTokens = themeButtonTokensCallback(state)
@@ -74,36 +79,93 @@ export const ButtonGroup = React.forwardRef(
74
79
  const shouldUseTransparentBackground =
75
80
  isMobileNonContained && !state.selected && !state.pressed && !state.hover && !state.focus
76
81
 
82
+ let widthStyle
83
+
84
+ switch (buttonWidthVariant) {
85
+ case EQUAL_WIDTH:
86
+ widthStyle = staticStyles.equalWidth
87
+ break
88
+ case RESPONSIVE_WIDTH:
89
+ widthStyle = staticStyles.responsiveWidth
90
+ break
91
+ // no default
92
+ }
93
+
77
94
  return {
78
95
  ...themeButtonTokens,
79
- ...(variant?.width === 'equal' && staticStyles.equalWidth),
96
+ ...widthStyle,
80
97
  ...(shouldUseTransparentBackground && { backgroundColor: 'transparent' }),
81
98
  alignSelf: themeButtonTokens.width ? 'flex-start' : 'center'
82
99
  }
83
100
  },
84
- [themeButtonTokensCallback, isMobileNonContained, variant?.width]
101
+ [themeButtonTokensCallback, isMobileNonContained, buttonWidthVariant]
85
102
  )
86
103
 
87
- const fieldsetStyles = useMemo(
88
- () => ({
89
- ...staticStyles.fieldsetBase,
104
+ const fieldsetStyles = useMemo(() => {
105
+ let fieldSetBase
106
+ switch (buttonWidthVariant) {
107
+ case EQUAL_WIDTH:
108
+ case RESPONSIVE_WIDTH:
109
+ fieldSetBase = staticStyles.fieldSetResponsive
110
+ break
111
+ default:
112
+ fieldSetBase = staticStyles.fieldsetBase
113
+ break
114
+ }
115
+
116
+ return {
117
+ ...fieldSetBase,
90
118
  borderRadius,
91
119
  backgroundColor: isMobileNonContained ? 'transparent' : backgroundColor || 'transparent',
92
- padding,
93
- width: variant?.width === 'equal' ? '100%' : 'auto'
94
- }),
95
- [borderRadius, backgroundColor, padding, variant?.width, isMobileNonContained]
96
- )
120
+ padding
121
+ }
122
+ }, [borderRadius, backgroundColor, padding, isMobileNonContained, buttonWidthVariant])
97
123
 
98
- const viewStyles = useMemo(
99
- () => ({
100
- ...staticStyles.viewBase,
124
+ const buttonsContainerStyles = useMemo(() => {
125
+ let flexWrap
126
+ if (isWeb) {
127
+ flexWrap = 'wrap'
128
+ } else {
129
+ switch (buttonWidthVariant) {
130
+ case EQUAL_WIDTH:
131
+ case RESPONSIVE_WIDTH:
132
+ flexWrap = 'nowrap'
133
+ break
134
+ default:
135
+ flexWrap = 'wrap'
136
+ break
137
+ }
138
+ }
139
+
140
+ let justifyContent
141
+ switch (buttonWidthVariant) {
142
+ case EQUAL_WIDTH:
143
+ justifyContent = 'space-evenly'
144
+ break
145
+ default:
146
+ justifyContent = 'flex-start'
147
+ break
148
+ }
149
+
150
+ let viewBaseStyle
151
+ switch (buttonWidthVariant) {
152
+ case EQUAL_WIDTH:
153
+ case RESPONSIVE_WIDTH:
154
+ viewBaseStyle = {}
155
+ break
156
+ default:
157
+ viewBaseStyle = staticStyles.viewBase
158
+ break
159
+ }
160
+
161
+ return {
162
+ ...viewBaseStyle,
163
+ flexWrap,
101
164
  flexDirection: direction === 'column' ? 'column' : 'row',
102
165
  gap: gapValue || 0,
103
- justifyContent: variant?.width === 'equal' ? 'space-evenly' : 'flex-start'
104
- }),
105
- [direction, gapValue, variant?.width]
106
- )
166
+ justifyContent
167
+ }
168
+ }, [direction, gapValue, buttonWidthVariant, isWeb])
107
169
 
108
170
  const { currentValues, toggleOneValue } = useMultipleInputValues({
109
171
  initialValues,
@@ -125,7 +187,7 @@ export const ButtonGroup = React.forwardRef(
125
187
  }
126
188
 
127
189
  // Some web screenreaders e.g. MacOS Voiceover don't handle radiogroups properly unless radio is direct child of radiogroup
128
- const innerRole =
190
+ const buttonsWrapperRole =
129
191
  Platform.OS === 'web' && accessibilityRole === 'radiogroup' ? accessibilityRole : undefined
130
192
 
131
193
  return (
@@ -146,7 +208,7 @@ export const ButtonGroup = React.forwardRef(
146
208
  style={fieldsetStyles}
147
209
  {...selectProps(rest)}
148
210
  >
149
- <View accessibilityRole={innerRole} style={viewStyles}>
211
+ <View accessibilityRole={buttonsWrapperRole} style={buttonsContainerStyles}>
150
212
  {items.map(
151
213
  (
152
214
  {
@@ -326,17 +388,24 @@ ButtonGroup.propTypes = {
326
388
  }
327
389
 
328
390
  const staticStyles = StyleSheet.create({
391
+ equalWidth: {
392
+ width: '100%',
393
+ flex: 1
394
+ },
395
+ responsiveWidth: {
396
+ width: 'auto'
397
+ },
329
398
  fieldsetBase: {
330
399
  alignSelf: 'flex-start',
331
- display: 'inline'
400
+ display: 'inline',
401
+ width: 'auto'
402
+ },
403
+ fieldSetResponsive: {
404
+ width: '100%'
332
405
  },
333
406
  viewBase: {
334
407
  flexWrap: 'wrap',
335
408
  alignContent: 'stretch',
336
409
  alignItems: 'center'
337
- },
338
- equalWidth: {
339
- width: '100%',
340
- flex: 1
341
410
  }
342
411
  })