@telus-uds/components-base 3.14.2 → 3.15.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.
package/CHANGELOG.md CHANGED
@@ -1,17 +1,21 @@
1
1
  # Change Log - @telus-uds/components-base
2
2
 
3
- This log was last generated on Fri, 05 Sep 2025 18:30:47 GMT and should not be manually modified.
3
+ This log was last generated on Wed, 10 Sep 2025 05:55:42 GMT and should not be manually modified.
4
4
 
5
5
  <!-- Start content -->
6
6
 
7
- ## 3.14.2
7
+ ## 3.15.0
8
8
 
9
- Fri, 05 Sep 2025 18:30:47 GMT
9
+ Wed, 10 Sep 2025 05:55:42 GMT
10
+
11
+ ### Minor changes
12
+
13
+ - `MultiSelectFilter`: Positioned fixed instead of absolute on web now (oscar.palencia@telus.com)
10
14
 
11
15
  ### Patches
12
16
 
13
- - `FlexGrid`: fix maxWidth prop (guillermo.peitzner@telus.com)
14
- - `FlexGrid`: change default limitWidth value prop to true (guillermo.peitzner@telus.com)
17
+ - `Select`: incorrect position rendering dropdown in mobile devices fixed (35577399+JoshHC@users.noreply.github.com)
18
+ - `Expand-Collapse`: Fixing content rendering problems when nesting panels (oscar.palencia@telus.com)
15
19
 
16
20
  ## 3.13.0
17
21
 
@@ -128,6 +128,10 @@ const ExpandCollapsePanel = /*#__PURE__*/_react.default.forwardRef((_ref5, ref)
128
128
  const themeTokens = (0, _ThemeProvider.useThemeTokens)('ExpandCollapsePanel', tokens, variant, {
129
129
  expanded: isExpanded
130
130
  });
131
+
132
+ // on mobile devices we require a scroll buffer equal to the font size
133
+ // to avoid triggering scrolling unnecessarily
134
+ const mobileScrollBuffer = _Platform.default.OS === 'web' ? 0 : selectTextStyles(themeTokens)?.fontSize;
131
135
  const handleControlPress = event => {
132
136
  onToggle?.(panelId, event);
133
137
  if (onPress) onPress(panelId, event);
@@ -139,7 +143,7 @@ const ExpandCollapsePanel = /*#__PURE__*/_react.default.forwardRef((_ref5, ref)
139
143
  } = {}
140
144
  } = event.nativeEvent;
141
145
  if (_Platform.default.OS === 'web' || _Platform.default.OS !== 'web' && containerHeight === null) {
142
- setContainerHeight(height);
146
+ setContainerHeight(height + mobileScrollBuffer);
143
147
  }
144
148
  };
145
149
  const [animatedStyles, animatedRef] = (0, _utils.useVerticalExpandAnimation)({
@@ -203,7 +207,7 @@ const ExpandCollapsePanel = /*#__PURE__*/_react.default.forwardRef((_ref5, ref)
203
207
  children: children
204
208
  }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_ScrollView.default, {
205
209
  onContentSizeChange: (_, height) => {
206
- setContainerHeight(height);
210
+ setContainerHeight(height + mobileScrollBuffer);
207
211
  },
208
212
  style: selectContainerStyles(themeTokens),
209
213
  accessibilityLabel: subPanelAccessibilityLabel,
@@ -216,7 +220,16 @@ const ExpandCollapsePanel = /*#__PURE__*/_react.default.forwardRef((_ref5, ref)
216
220
  ExpandCollapsePanel.displayName = 'ExpandCollapsePanel';
217
221
  const staticStyles = _StyleSheet.default.create({
218
222
  container: {
219
- flex: 1,
223
+ ..._Platform.default.select({
224
+ web: {
225
+ flexGrow: 1,
226
+ flexshrink: 1,
227
+ flexbasis: 'auto'
228
+ },
229
+ default: {
230
+ flex: 1
231
+ }
232
+ }),
220
233
  justifyContent: 'flex-start'
221
234
  },
222
235
  panelContainer: {
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.default = void 0;
7
7
  var _react = _interopRequireDefault(require("react"));
8
8
  var _propTypes = _interopRequireDefault(require("prop-types"));
9
+ var _portal = require("@gorhom/portal");
9
10
  var _View = _interopRequireDefault(require("react-native-web/dist/cjs/exports/View"));
10
11
  var _StyleSheet = _interopRequireDefault(require("react-native-web/dist/cjs/exports/StyleSheet"));
11
12
  var _Dimensions = _interopRequireDefault(require("react-native-web/dist/cjs/exports/Dimensions"));
@@ -214,6 +215,20 @@ const MultiSelectFilter = /*#__PURE__*/_react.default.forwardRef((_ref3, ref) =>
214
215
  setIsOpen(false);
215
216
  onCancel();
216
217
  };
218
+ const appRootRef = _react.default.useRef(null);
219
+ const [rootOffsets, setRootOffsets] = _react.default.useState(null);
220
+ _react.default.useEffect(() => {
221
+ if (rootOffsets) return;
222
+ appRootRef.current?.measureInWindow((x, y) => {
223
+ // Only set offsets if they are positive
224
+ // this is because we want to avoid negative offsets that could cause
225
+ // the dropdown to be positioned incorrectly in some situations
226
+ if (y > 0) setRootOffsets({
227
+ horizontal: x,
228
+ vertical: y
229
+ });
230
+ });
231
+ }, [isOpen, rootOffsets]);
217
232
  const {
218
233
  align,
219
234
  offsets
@@ -230,7 +245,8 @@ const MultiSelectFilter = /*#__PURE__*/_react.default.forwardRef((_ref3, ref) =>
230
245
  left: 'left'
231
246
  },
232
247
  offsets: {
233
- vertical: 4
248
+ vertical: 4 - (rootOffsets?.vertical || 0),
249
+ horizontal: -rootOffsets?.horizontal || 0
234
250
  }
235
251
  }
236
252
  });
@@ -367,7 +383,12 @@ const MultiSelectFilter = /*#__PURE__*/_react.default.forwardRef((_ref3, ref) =>
367
383
  })]
368
384
  });
369
385
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
370
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_Button.ButtonDropdown, {
386
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_portal.Portal, {
387
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_View.default, {
388
+ ref: appRootRef,
389
+ style: styles.appRootRef
390
+ })
391
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_Button.ButtonDropdown, {
371
392
  ref: sourceRef,
372
393
  ...pressHandlers,
373
394
  value: isOpen,
@@ -448,6 +469,11 @@ const styles = _StyleSheet.default.create({
448
469
  },
449
470
  scrollContainer: {
450
471
  padding: 1
472
+ },
473
+ appRootRef: {
474
+ position: 'absolute',
475
+ top: 0,
476
+ left: 0
451
477
  }
452
478
  });
453
479
 
@@ -9,6 +9,7 @@ var _View = _interopRequireDefault(require("react-native-web/dist/cjs/exports/Vi
9
9
  var _Platform = _interopRequireDefault(require("react-native-web/dist/cjs/exports/Platform"));
10
10
  var _StyleSheet = _interopRequireDefault(require("react-native-web/dist/cjs/exports/StyleSheet"));
11
11
  var _propTypes = _interopRequireDefault(require("prop-types"));
12
+ var _systemConstants = require("@telus-uds/system-constants");
12
13
  var _ThemeProvider = require("../ThemeProvider");
13
14
  var _utils = require("../utils");
14
15
  var _Picker = _interopRequireDefault(require("./Picker"));
@@ -55,7 +56,15 @@ const selectInputStyles = (_ref, themeOptions, inactive) => {
55
56
  // since iOS Safari needs a prefix
56
57
  outline: 'none',
57
58
  cursor: inactive ? 'not-allowed' : undefined,
58
- opacity: inactive ? 1 : undefined // override Chrome's default fadeout of a disabled select
59
+ opacity: inactive ? 1 : undefined,
60
+ // override Chrome's default fadeout of a disabled select
61
+ // Enhanced fix for mobile dropdown positioning: restore native appearance on touch devices
62
+ // Using multiple media queries and higher specificity to ensure it works in all contexts
63
+ [`@media (pointer: coarse), (maxWidth: ${_systemConstants.viewports.map.get(_systemConstants.viewports.md)}px) and (hover: none)`]: {
64
+ appearance: 'auto !important',
65
+ WebkitAppearance: 'auto !important',
66
+ backgroundImage: 'none !important'
67
+ }
59
68
  }
60
69
  });
61
70
  let paddingWithIcons = paddingRight;
@@ -9,7 +9,7 @@ var _Dimensions = _interopRequireDefault(require("react-native-web/dist/cjs/expo
9
9
  var _Platform = _interopRequireDefault(require("react-native-web/dist/cjs/exports/Platform"));
10
10
  var _lodash = _interopRequireDefault(require("lodash.debounce"));
11
11
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
12
- const DEBOUNCE_DELAY = 100;
12
+ const DEBOUNCE_DELAY = 300;
13
13
  const adjustHorizontalToFit = (initialOffset, windowWidth, sourceWidth) => {
14
14
  const offset = Math.max(0, initialOffset);
15
15
  const otherEdgeOverflow = Math.max(0, offset + sourceWidth - windowWidth);
@@ -71,34 +71,36 @@ function getOverlaidPosition(_ref2) {
71
71
 
72
72
  // Will have top, bottom, left and/or right offsets depending on `align`
73
73
  const positioning = {};
74
+ const verticalOffset = offsets.vertical ?? 0;
75
+ const horizontalOffset = offsets.horizontal ?? 0;
74
76
  if (align.top) positioning.top = getPosition({
75
77
  edge: getEdgeType(align, 'top'),
76
- fromEdge: sourceLayout.y + scrollY + (offsets.vertical ?? 0),
78
+ fromEdge: sourceLayout.y + scrollY + verticalOffset,
77
79
  sourceSize: sourceLayout.height
78
80
  });
79
81
  if (align.middle) positioning.top = getPosition({
80
82
  edge: getEdgeType(align, 'middle'),
81
- fromEdge: sourceLayout.y + scrollY + (offsets.vertical ?? 0) - targetDimensions.height / 2,
83
+ fromEdge: sourceLayout.y + scrollY + verticalOffset - targetDimensions.height / 2,
82
84
  sourceSize: sourceLayout.height
83
85
  });
84
86
  if (align.bottom) positioning.bottom = getPosition({
85
87
  edge: getEdgeType(align, 'bottom'),
86
- fromEdge: windowDimensions.height - (sourceLayout.y + scrollY + sourceLayout.height - (offsets.vertical ?? 0)),
88
+ fromEdge: windowDimensions.height - (sourceLayout.y + scrollY + sourceLayout.height - verticalOffset),
87
89
  sourceSize: sourceLayout.height
88
90
  });
89
91
  if (align.left) positioning.left = getPosition({
90
92
  edge: getEdgeType(align, 'left'),
91
- fromEdge: sourceLayout.x + scrollX + (offsets.horizontal ?? 0),
93
+ fromEdge: sourceLayout.x + scrollX + horizontalOffset,
92
94
  sourceSize: sourceLayout.width
93
95
  });
94
96
  if (align.center) positioning.left = getPosition({
95
97
  edge: getEdgeType(align, 'center'),
96
- fromEdge: sourceLayout.x + scrollX + (offsets.horizontal ?? 0) - targetDimensions.width / 2,
98
+ fromEdge: sourceLayout.x + scrollX + horizontalOffset - targetDimensions.width / 2,
97
99
  sourceSize: sourceLayout.width
98
100
  });
99
101
  if (align.right) positioning.right = getPosition({
100
102
  edge: getEdgeType(align, 'right'),
101
- fromEdge: windowDimensions.width - (sourceLayout.x + scrollX + sourceLayout.width - (offsets.horizontal ?? 0)),
103
+ fromEdge: windowDimensions.width - (sourceLayout.x + scrollX + sourceLayout.width - horizontalOffset),
102
104
  sourceSize: sourceLayout.width
103
105
  });
104
106
  if (!(align.left && align.right)) {
@@ -121,6 +121,10 @@ const ExpandCollapsePanel = /*#__PURE__*/React.forwardRef((_ref5, ref) => {
121
121
  const themeTokens = useThemeTokens('ExpandCollapsePanel', tokens, variant, {
122
122
  expanded: isExpanded
123
123
  });
124
+
125
+ // on mobile devices we require a scroll buffer equal to the font size
126
+ // to avoid triggering scrolling unnecessarily
127
+ const mobileScrollBuffer = Platform.OS === 'web' ? 0 : selectTextStyles(themeTokens)?.fontSize;
124
128
  const handleControlPress = event => {
125
129
  onToggle?.(panelId, event);
126
130
  if (onPress) onPress(panelId, event);
@@ -132,7 +136,7 @@ const ExpandCollapsePanel = /*#__PURE__*/React.forwardRef((_ref5, ref) => {
132
136
  } = {}
133
137
  } = event.nativeEvent;
134
138
  if (Platform.OS === 'web' || Platform.OS !== 'web' && containerHeight === null) {
135
- setContainerHeight(height);
139
+ setContainerHeight(height + mobileScrollBuffer);
136
140
  }
137
141
  };
138
142
  const [animatedStyles, animatedRef] = useVerticalExpandAnimation({
@@ -196,7 +200,7 @@ const ExpandCollapsePanel = /*#__PURE__*/React.forwardRef((_ref5, ref) => {
196
200
  children: children
197
201
  }) : /*#__PURE__*/_jsx(ScrollView, {
198
202
  onContentSizeChange: (_, height) => {
199
- setContainerHeight(height);
203
+ setContainerHeight(height + mobileScrollBuffer);
200
204
  },
201
205
  style: selectContainerStyles(themeTokens),
202
206
  accessibilityLabel: subPanelAccessibilityLabel,
@@ -209,7 +213,16 @@ const ExpandCollapsePanel = /*#__PURE__*/React.forwardRef((_ref5, ref) => {
209
213
  ExpandCollapsePanel.displayName = 'ExpandCollapsePanel';
210
214
  const staticStyles = StyleSheet.create({
211
215
  container: {
212
- flex: 1,
216
+ ...Platform.select({
217
+ web: {
218
+ flexGrow: 1,
219
+ flexshrink: 1,
220
+ flexbasis: 'auto'
221
+ },
222
+ default: {
223
+ flex: 1
224
+ }
225
+ }),
213
226
  justifyContent: 'flex-start'
214
227
  },
215
228
  panelContainer: {
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
+ import { Portal } from '@gorhom/portal';
3
4
  import View from "react-native-web/dist/exports/View";
4
5
  import StyleSheet from "react-native-web/dist/exports/StyleSheet";
5
6
  import Dimensions from "react-native-web/dist/exports/Dimensions";
@@ -207,6 +208,20 @@ const MultiSelectFilter = /*#__PURE__*/React.forwardRef((_ref3, ref) => {
207
208
  setIsOpen(false);
208
209
  onCancel();
209
210
  };
211
+ const appRootRef = React.useRef(null);
212
+ const [rootOffsets, setRootOffsets] = React.useState(null);
213
+ React.useEffect(() => {
214
+ if (rootOffsets) return;
215
+ appRootRef.current?.measureInWindow((x, y) => {
216
+ // Only set offsets if they are positive
217
+ // this is because we want to avoid negative offsets that could cause
218
+ // the dropdown to be positioned incorrectly in some situations
219
+ if (y > 0) setRootOffsets({
220
+ horizontal: x,
221
+ vertical: y
222
+ });
223
+ });
224
+ }, [isOpen, rootOffsets]);
210
225
  const {
211
226
  align,
212
227
  offsets
@@ -223,7 +238,8 @@ const MultiSelectFilter = /*#__PURE__*/React.forwardRef((_ref3, ref) => {
223
238
  left: 'left'
224
239
  },
225
240
  offsets: {
226
- vertical: 4
241
+ vertical: 4 - (rootOffsets?.vertical || 0),
242
+ horizontal: -rootOffsets?.horizontal || 0
227
243
  }
228
244
  }
229
245
  });
@@ -360,7 +376,12 @@ const MultiSelectFilter = /*#__PURE__*/React.forwardRef((_ref3, ref) => {
360
376
  })]
361
377
  });
362
378
  return /*#__PURE__*/_jsxs(_Fragment, {
363
- children: [/*#__PURE__*/_jsx(ButtonDropdown, {
379
+ children: [/*#__PURE__*/_jsx(Portal, {
380
+ children: /*#__PURE__*/_jsx(View, {
381
+ ref: appRootRef,
382
+ style: styles.appRootRef
383
+ })
384
+ }), /*#__PURE__*/_jsx(ButtonDropdown, {
364
385
  ref: sourceRef,
365
386
  ...pressHandlers,
366
387
  value: isOpen,
@@ -441,6 +462,11 @@ const styles = StyleSheet.create({
441
462
  },
442
463
  scrollContainer: {
443
464
  padding: 1
465
+ },
466
+ appRootRef: {
467
+ position: 'absolute',
468
+ top: 0,
469
+ left: 0
444
470
  }
445
471
  });
446
472
 
@@ -3,6 +3,7 @@ import View from "react-native-web/dist/exports/View";
3
3
  import Platform from "react-native-web/dist/exports/Platform";
4
4
  import StyleSheet from "react-native-web/dist/exports/StyleSheet";
5
5
  import PropTypes from 'prop-types';
6
+ import { viewports } from '@telus-uds/system-constants';
6
7
  import { applyTextStyles, useThemeTokens, applyOuterBorder, useTheme } from '../ThemeProvider';
7
8
  import { a11yProps, componentPropType, getTokensPropType, inputSupportsProps, selectSystemProps, useInputValue, variantProp, viewProps, htmlAttrs } from '../utils';
8
9
  import Picker from './Picker';
@@ -48,7 +49,15 @@ const selectInputStyles = (_ref, themeOptions, inactive) => {
48
49
  // since iOS Safari needs a prefix
49
50
  outline: 'none',
50
51
  cursor: inactive ? 'not-allowed' : undefined,
51
- opacity: inactive ? 1 : undefined // override Chrome's default fadeout of a disabled select
52
+ opacity: inactive ? 1 : undefined,
53
+ // override Chrome's default fadeout of a disabled select
54
+ // Enhanced fix for mobile dropdown positioning: restore native appearance on touch devices
55
+ // Using multiple media queries and higher specificity to ensure it works in all contexts
56
+ [`@media (pointer: coarse), (maxWidth: ${viewports.map.get(viewports.md)}px) and (hover: none)`]: {
57
+ appearance: 'auto !important',
58
+ WebkitAppearance: 'auto !important',
59
+ backgroundImage: 'none !important'
60
+ }
52
61
  }
53
62
  });
54
63
  let paddingWithIcons = paddingRight;
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import Dimensions from "react-native-web/dist/exports/Dimensions";
3
3
  import Platform from "react-native-web/dist/exports/Platform";
4
4
  import debounce from 'lodash.debounce';
5
- const DEBOUNCE_DELAY = 100;
5
+ const DEBOUNCE_DELAY = 300;
6
6
  const adjustHorizontalToFit = (initialOffset, windowWidth, sourceWidth) => {
7
7
  const offset = Math.max(0, initialOffset);
8
8
  const otherEdgeOverflow = Math.max(0, offset + sourceWidth - windowWidth);
@@ -64,34 +64,36 @@ function getOverlaidPosition(_ref2) {
64
64
 
65
65
  // Will have top, bottom, left and/or right offsets depending on `align`
66
66
  const positioning = {};
67
+ const verticalOffset = offsets.vertical ?? 0;
68
+ const horizontalOffset = offsets.horizontal ?? 0;
67
69
  if (align.top) positioning.top = getPosition({
68
70
  edge: getEdgeType(align, 'top'),
69
- fromEdge: sourceLayout.y + scrollY + (offsets.vertical ?? 0),
71
+ fromEdge: sourceLayout.y + scrollY + verticalOffset,
70
72
  sourceSize: sourceLayout.height
71
73
  });
72
74
  if (align.middle) positioning.top = getPosition({
73
75
  edge: getEdgeType(align, 'middle'),
74
- fromEdge: sourceLayout.y + scrollY + (offsets.vertical ?? 0) - targetDimensions.height / 2,
76
+ fromEdge: sourceLayout.y + scrollY + verticalOffset - targetDimensions.height / 2,
75
77
  sourceSize: sourceLayout.height
76
78
  });
77
79
  if (align.bottom) positioning.bottom = getPosition({
78
80
  edge: getEdgeType(align, 'bottom'),
79
- fromEdge: windowDimensions.height - (sourceLayout.y + scrollY + sourceLayout.height - (offsets.vertical ?? 0)),
81
+ fromEdge: windowDimensions.height - (sourceLayout.y + scrollY + sourceLayout.height - verticalOffset),
80
82
  sourceSize: sourceLayout.height
81
83
  });
82
84
  if (align.left) positioning.left = getPosition({
83
85
  edge: getEdgeType(align, 'left'),
84
- fromEdge: sourceLayout.x + scrollX + (offsets.horizontal ?? 0),
86
+ fromEdge: sourceLayout.x + scrollX + horizontalOffset,
85
87
  sourceSize: sourceLayout.width
86
88
  });
87
89
  if (align.center) positioning.left = getPosition({
88
90
  edge: getEdgeType(align, 'center'),
89
- fromEdge: sourceLayout.x + scrollX + (offsets.horizontal ?? 0) - targetDimensions.width / 2,
91
+ fromEdge: sourceLayout.x + scrollX + horizontalOffset - targetDimensions.width / 2,
90
92
  sourceSize: sourceLayout.width
91
93
  });
92
94
  if (align.right) positioning.right = getPosition({
93
95
  edge: getEdgeType(align, 'right'),
94
- fromEdge: windowDimensions.width - (sourceLayout.x + scrollX + sourceLayout.width - (offsets.horizontal ?? 0)),
96
+ fromEdge: windowDimensions.width - (sourceLayout.x + scrollX + sourceLayout.width - horizontalOffset),
95
97
  sourceSize: sourceLayout.width
96
98
  });
97
99
  if (!(align.left && align.right)) {
package/lib/package.json CHANGED
@@ -84,6 +84,6 @@
84
84
  "standard-engine": {
85
85
  "skip": true
86
86
  },
87
- "version": "3.14.2",
87
+ "version": "3.15.0",
88
88
  "types": "types/index.d.ts"
89
89
  }
package/package.json CHANGED
@@ -84,6 +84,6 @@
84
84
  "standard-engine": {
85
85
  "skip": true
86
86
  },
87
- "version": "3.14.2",
87
+ "version": "3.15.0",
88
88
  "types": "types/index.d.ts"
89
89
  }
@@ -116,6 +116,10 @@ const ExpandCollapsePanel = React.forwardRef(
116
116
  expanded: isExpanded
117
117
  })
118
118
 
119
+ // on mobile devices we require a scroll buffer equal to the font size
120
+ // to avoid triggering scrolling unnecessarily
121
+ const mobileScrollBuffer = Platform.OS === 'web' ? 0 : selectTextStyles(themeTokens)?.fontSize
122
+
119
123
  const handleControlPress = (event) => {
120
124
  onToggle?.(panelId, event)
121
125
  if (onPress) onPress(panelId, event)
@@ -124,7 +128,7 @@ const ExpandCollapsePanel = React.forwardRef(
124
128
  const onContainerLayout = (event) => {
125
129
  const { layout: { height = 0 } = {} } = event.nativeEvent
126
130
  if (Platform.OS === 'web' || (Platform.OS !== 'web' && containerHeight === null)) {
127
- setContainerHeight(height)
131
+ setContainerHeight(height + mobileScrollBuffer)
128
132
  }
129
133
  }
130
134
 
@@ -208,7 +212,7 @@ const ExpandCollapsePanel = React.forwardRef(
208
212
  ) : (
209
213
  <ScrollView
210
214
  onContentSizeChange={(_, height) => {
211
- setContainerHeight(height)
215
+ setContainerHeight(height + mobileScrollBuffer)
212
216
  }}
213
217
  style={selectContainerStyles(themeTokens)}
214
218
  accessibilityLabel={subPanelAccessibilityLabel}
@@ -226,7 +230,14 @@ ExpandCollapsePanel.displayName = 'ExpandCollapsePanel'
226
230
 
227
231
  const staticStyles = StyleSheet.create({
228
232
  container: {
229
- flex: 1,
233
+ ...Platform.select({
234
+ web: {
235
+ flexGrow: 1,
236
+ flexshrink: 1,
237
+ flexbasis: 'auto'
238
+ },
239
+ default: { flex: 1 }
240
+ }),
230
241
  justifyContent: 'flex-start'
231
242
  },
232
243
  panelContainer: {
@@ -1,6 +1,6 @@
1
1
  import React from 'react'
2
2
  import PropTypes from 'prop-types'
3
-
3
+ import { Portal } from '@gorhom/portal'
4
4
  import { View, StyleSheet, Dimensions, SafeAreaView, Platform, ScrollView } from 'react-native'
5
5
  import { useThemeTokens, useThemeTokensCallback, applyTextStyles } from '../ThemeProvider'
6
6
  import {
@@ -215,11 +215,27 @@ const MultiSelectFilter = React.forwardRef(
215
215
  onCancel()
216
216
  }
217
217
 
218
+ const appRootRef = React.useRef(null)
219
+ const [rootOffsets, setRootOffsets] = React.useState(null)
220
+
221
+ React.useEffect(() => {
222
+ if (rootOffsets) return
223
+ appRootRef.current?.measureInWindow((x, y) => {
224
+ // Only set offsets if they are positive
225
+ // this is because we want to avoid negative offsets that could cause
226
+ // the dropdown to be positioned incorrectly in some situations
227
+ if (y > 0) setRootOffsets({ horizontal: x, vertical: y })
228
+ })
229
+ }, [isOpen, rootOffsets])
230
+
218
231
  const { align, offsets } = useResponsiveProp({
219
232
  xs: { align: { top: 'top', left: 'left' } },
220
233
  sm: {
221
234
  align: { top: 'bottom', left: 'left' },
222
- offsets: { vertical: 4 }
235
+ offsets: {
236
+ vertical: 4 - (rootOffsets?.vertical || 0),
237
+ horizontal: -rootOffsets?.horizontal || 0
238
+ }
223
239
  }
224
240
  })
225
241
 
@@ -345,6 +361,14 @@ const MultiSelectFilter = React.forwardRef(
345
361
 
346
362
  return (
347
363
  <>
364
+ {/*
365
+ This View is rendered inside a Portal to determine the application's root position.
366
+ Capturing it is crucial to determine offsets to position the Modal correctly, preventing
367
+ misalignment that can occur when the Portal does not render the Modal at the body level.
368
+ */}
369
+ <Portal>
370
+ <View ref={appRootRef} style={styles.appRootRef} />
371
+ </Portal>
348
372
  <ButtonDropdown
349
373
  ref={sourceRef}
350
374
  key={id}
@@ -434,6 +458,11 @@ const styles = StyleSheet.create({
434
458
  },
435
459
  scrollContainer: {
436
460
  padding: 1
461
+ },
462
+ appRootRef: {
463
+ position: 'absolute',
464
+ top: 0,
465
+ left: 0
437
466
  }
438
467
  })
439
468
 
@@ -2,6 +2,7 @@ import React from 'react'
2
2
 
3
3
  import { View, Platform, StyleSheet } from 'react-native'
4
4
  import PropTypes from 'prop-types'
5
+ import { viewports } from '@telus-uds/system-constants'
5
6
  import { applyTextStyles, useThemeTokens, applyOuterBorder, useTheme } from '../ThemeProvider'
6
7
  import {
7
8
  a11yProps,
@@ -69,7 +70,16 @@ const selectInputStyles = (
69
70
  WebkitAppearance: 'none', // since iOS Safari needs a prefix
70
71
  outline: 'none',
71
72
  cursor: inactive ? 'not-allowed' : undefined,
72
- opacity: inactive ? 1 : undefined // override Chrome's default fadeout of a disabled select
73
+ opacity: inactive ? 1 : undefined, // override Chrome's default fadeout of a disabled select
74
+ // Enhanced fix for mobile dropdown positioning: restore native appearance on touch devices
75
+ // Using multiple media queries and higher specificity to ensure it works in all contexts
76
+ [`@media (pointer: coarse), (maxWidth: ${viewports.map.get(
77
+ viewports.md
78
+ )}px) and (hover: none)`]: {
79
+ appearance: 'auto !important',
80
+ WebkitAppearance: 'auto !important',
81
+ backgroundImage: 'none !important'
82
+ }
73
83
  }
74
84
  })
75
85
 
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
2
2
  import { Dimensions, Platform } from 'react-native'
3
3
  import debounce from 'lodash.debounce'
4
4
 
5
- const DEBOUNCE_DELAY = 100
5
+ const DEBOUNCE_DELAY = 300
6
6
 
7
7
  const adjustHorizontalToFit = (initialOffset, windowWidth, sourceWidth) => {
8
8
  const offset = Math.max(0, initialOffset)
@@ -62,45 +62,46 @@ function getOverlaidPosition({
62
62
  // Will have top, bottom, left and/or right offsets depending on `align`
63
63
  const positioning = {}
64
64
 
65
+ const verticalOffset = offsets.vertical ?? 0
66
+ const horizontalOffset = offsets.horizontal ?? 0
67
+
65
68
  if (align.top)
66
69
  positioning.top = getPosition({
67
70
  edge: getEdgeType(align, 'top'),
68
- fromEdge: sourceLayout.y + scrollY + (offsets.vertical ?? 0),
71
+ fromEdge: sourceLayout.y + scrollY + verticalOffset,
69
72
  sourceSize: sourceLayout.height
70
73
  })
71
74
  if (align.middle)
72
75
  positioning.top = getPosition({
73
76
  edge: getEdgeType(align, 'middle'),
74
- fromEdge: sourceLayout.y + scrollY + (offsets.vertical ?? 0) - targetDimensions.height / 2,
77
+ fromEdge: sourceLayout.y + scrollY + verticalOffset - targetDimensions.height / 2,
75
78
  sourceSize: sourceLayout.height
76
79
  })
77
80
  if (align.bottom)
78
81
  positioning.bottom = getPosition({
79
82
  edge: getEdgeType(align, 'bottom'),
80
83
  fromEdge:
81
- windowDimensions.height -
82
- (sourceLayout.y + scrollY + sourceLayout.height - (offsets.vertical ?? 0)),
84
+ windowDimensions.height - (sourceLayout.y + scrollY + sourceLayout.height - verticalOffset),
83
85
  sourceSize: sourceLayout.height
84
86
  })
85
87
 
86
88
  if (align.left)
87
89
  positioning.left = getPosition({
88
90
  edge: getEdgeType(align, 'left'),
89
- fromEdge: sourceLayout.x + scrollX + (offsets.horizontal ?? 0),
91
+ fromEdge: sourceLayout.x + scrollX + horizontalOffset,
90
92
  sourceSize: sourceLayout.width
91
93
  })
92
94
  if (align.center)
93
95
  positioning.left = getPosition({
94
96
  edge: getEdgeType(align, 'center'),
95
- fromEdge: sourceLayout.x + scrollX + (offsets.horizontal ?? 0) - targetDimensions.width / 2,
97
+ fromEdge: sourceLayout.x + scrollX + horizontalOffset - targetDimensions.width / 2,
96
98
  sourceSize: sourceLayout.width
97
99
  })
98
100
  if (align.right)
99
101
  positioning.right = getPosition({
100
102
  edge: getEdgeType(align, 'right'),
101
103
  fromEdge:
102
- windowDimensions.width -
103
- (sourceLayout.x + scrollX + sourceLayout.width - (offsets.horizontal ?? 0)),
104
+ windowDimensions.width - (sourceLayout.x + scrollX + sourceLayout.width - horizontalOffset),
104
105
  sourceSize: sourceLayout.width
105
106
  })
106
107