@telus-uds/components-base 3.30.0 → 3.31.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 +15 -1
- package/lib/cjs/Autocomplete/Autocomplete.js +13 -1
- package/lib/cjs/Autocomplete/constants.js +1 -1
- package/lib/cjs/Listbox/ListboxOverlay.js +7 -1
- package/lib/cjs/MultiSelectFilter/ModalOverlay.js +7 -1
- package/lib/cjs/MultiSelectFilter/MultiSelectFilter.js +2 -28
- package/lib/cjs/TextInput/TextInputBase.js +2 -1
- package/lib/cjs/utils/useOverlaidPosition.js +83 -42
- package/lib/esm/Autocomplete/Autocomplete.js +13 -1
- package/lib/esm/Autocomplete/constants.js +1 -1
- package/lib/esm/Listbox/ListboxOverlay.js +7 -1
- package/lib/esm/MultiSelectFilter/ModalOverlay.js +8 -2
- package/lib/esm/MultiSelectFilter/MultiSelectFilter.js +2 -28
- package/lib/esm/TextInput/TextInputBase.js +2 -1
- package/lib/esm/utils/useOverlaidPosition.js +83 -42
- package/lib/package.json +1 -1
- package/package.json +1 -1
- package/src/Autocomplete/Autocomplete.jsx +11 -2
- package/src/Autocomplete/constants.js +1 -1
- package/src/Listbox/ListboxOverlay.jsx +6 -2
- package/src/MultiSelectFilter/ModalOverlay.jsx +9 -3
- package/src/MultiSelectFilter/MultiSelectFilter.jsx +1 -31
- package/src/TextInput/TextInputBase.jsx +2 -1
- package/src/utils/useOverlaidPosition.js +84 -34
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
# Change Log - @telus-uds/components-base
|
|
2
2
|
|
|
3
|
-
This log was last generated on
|
|
3
|
+
This log was last generated on Fri, 22 May 2026 05:09:10 GMT and should not be manually modified.
|
|
4
4
|
|
|
5
5
|
<!-- Start content -->
|
|
6
6
|
|
|
7
|
+
## 3.31.0
|
|
8
|
+
|
|
9
|
+
Fri, 22 May 2026 05:09:10 GMT
|
|
10
|
+
|
|
11
|
+
### Minor changes
|
|
12
|
+
|
|
13
|
+
- `Autocomplete`: Add scroll to overlay in mobile (david.melara1@telus.com)
|
|
14
|
+
|
|
15
|
+
### Patches
|
|
16
|
+
|
|
17
|
+
- `MultiSelectFilter`: position issues fixed in MFE (josue.higueroscalderon@telus.com)
|
|
18
|
+
- `TextInput`: Fix `onChangeText` returning `undefined` instead of an empty string when `isNumeric` is set and no text is provided (david.melara1@telus.com)
|
|
19
|
+
- `Autocomplete`: inside modal story fixed to show the close button properly (josue.higueroscalderon@telus.com)
|
|
20
|
+
|
|
7
21
|
## 3.30.0
|
|
8
22
|
|
|
9
23
|
Thu, 16 Apr 2026 21:04:33 GMT
|
|
@@ -126,6 +126,7 @@ const Autocomplete = /*#__PURE__*/_react.default.forwardRef((_ref2, ref) => {
|
|
|
126
126
|
value,
|
|
127
127
|
helpText = '',
|
|
128
128
|
loadingLabel,
|
|
129
|
+
dropdownPosition = 'bottom',
|
|
129
130
|
tokens,
|
|
130
131
|
...rest
|
|
131
132
|
} = _ref2;
|
|
@@ -181,6 +182,13 @@ const Autocomplete = /*#__PURE__*/_react.default.forwardRef((_ref2, ref) => {
|
|
|
181
182
|
isShown: isExpanded || hintExpansionEnabled,
|
|
182
183
|
offsets: {
|
|
183
184
|
vertical: _Platform.default.OS !== 'web' && (hint || inputLabel) ? 28 : 4
|
|
185
|
+
},
|
|
186
|
+
align: dropdownPosition === 'top' ? {
|
|
187
|
+
center: 'center',
|
|
188
|
+
bottom: 'top'
|
|
189
|
+
} : {
|
|
190
|
+
center: 'center',
|
|
191
|
+
top: 'bottom'
|
|
184
192
|
}
|
|
185
193
|
});
|
|
186
194
|
const targetRef = _react.default.useRef(null);
|
|
@@ -533,6 +541,10 @@ Autocomplete.propTypes = {
|
|
|
533
541
|
/**
|
|
534
542
|
* Input value for controlled usage
|
|
535
543
|
*/
|
|
536
|
-
value: _propTypes.default.string
|
|
544
|
+
value: _propTypes.default.string,
|
|
545
|
+
/**
|
|
546
|
+
* Controls whether the dropdown renders above or below the input. Defaults to 'bottom'.
|
|
547
|
+
*/
|
|
548
|
+
dropdownPosition: _propTypes.default.oneOf(['top', 'bottom'])
|
|
537
549
|
};
|
|
538
550
|
var _default = exports.default = Autocomplete;
|
|
@@ -6,6 +6,6 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
exports.MIN_LISTBOX_WIDTH = exports.INPUT_LEFT_PADDING = exports.DEFAULT_MIN_TO_SUGGESTION = exports.DEFAULT_MAX_SUGGESTIONS = exports.DEFAULT_MAX_DROPDOWN_HEIGHT = void 0;
|
|
7
7
|
const DEFAULT_MIN_TO_SUGGESTION = exports.DEFAULT_MIN_TO_SUGGESTION = 1;
|
|
8
8
|
const DEFAULT_MAX_SUGGESTIONS = exports.DEFAULT_MAX_SUGGESTIONS = 5;
|
|
9
|
-
const DEFAULT_MAX_DROPDOWN_HEIGHT = exports.DEFAULT_MAX_DROPDOWN_HEIGHT =
|
|
9
|
+
const DEFAULT_MAX_DROPDOWN_HEIGHT = exports.DEFAULT_MAX_DROPDOWN_HEIGHT = 500;
|
|
10
10
|
const INPUT_LEFT_PADDING = exports.INPUT_LEFT_PADDING = 16;
|
|
11
11
|
const MIN_LISTBOX_WIDTH = exports.MIN_LISTBOX_WIDTH = 288;
|
|
@@ -9,6 +9,7 @@ var _propTypes = _interopRequireDefault(require("prop-types"));
|
|
|
9
9
|
var _View = _interopRequireDefault(require("react-native-web/dist/cjs/exports/View"));
|
|
10
10
|
var _StyleSheet = _interopRequireDefault(require("react-native-web/dist/cjs/exports/StyleSheet"));
|
|
11
11
|
var _Platform = _interopRequireDefault(require("react-native-web/dist/cjs/exports/Platform"));
|
|
12
|
+
var _ScrollView = _interopRequireDefault(require("react-native-web/dist/cjs/exports/ScrollView"));
|
|
12
13
|
var _Portal = _interopRequireDefault(require("../Portal"));
|
|
13
14
|
var _ThemeProvider = require("../ThemeProvider");
|
|
14
15
|
var _Card = _interopRequireDefault(require("../Card"));
|
|
@@ -67,7 +68,12 @@ const DropdownOverlay = /*#__PURE__*/_react.default.forwardRef((_ref, ref) => {
|
|
|
67
68
|
paddingLeft: paddingHorizontal,
|
|
68
69
|
paddingRight: paddingHorizontal
|
|
69
70
|
},
|
|
70
|
-
children:
|
|
71
|
+
children: _Platform.default.OS !== 'web' ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_ScrollView.default, {
|
|
72
|
+
style: {
|
|
73
|
+
maxHeight
|
|
74
|
+
},
|
|
75
|
+
children: children
|
|
76
|
+
}) : children
|
|
71
77
|
})
|
|
72
78
|
});
|
|
73
79
|
});
|
|
@@ -10,6 +10,7 @@ var _View = _interopRequireDefault(require("react-native-web/dist/cjs/exports/Vi
|
|
|
10
10
|
var _StyleSheet = _interopRequireDefault(require("react-native-web/dist/cjs/exports/StyleSheet"));
|
|
11
11
|
var _Platform = _interopRequireDefault(require("react-native-web/dist/cjs/exports/Platform"));
|
|
12
12
|
var _portal = require("@gorhom/portal");
|
|
13
|
+
var _Portal = _interopRequireDefault(require("../Portal/Portal"));
|
|
13
14
|
var _utils = require("../utils");
|
|
14
15
|
var _ViewportProvider = require("../ViewportProvider");
|
|
15
16
|
var _ThemeProvider = require("../ThemeProvider");
|
|
@@ -123,7 +124,12 @@ const ModalOverlay = /*#__PURE__*/_react.default.forwardRef((_ref2, ref) => {
|
|
|
123
124
|
copy
|
|
124
125
|
});
|
|
125
126
|
const closeLabel = getCopy('closeButton');
|
|
126
|
-
|
|
127
|
+
|
|
128
|
+
// On web, use the local Portal (always appends to document.body) to ensure correct
|
|
129
|
+
// absolute positioning in MFE/iframe contexts. @gorhom/portal may render its host
|
|
130
|
+
// outside the iframe's document, causing coordinate mismatches when scrolled.
|
|
131
|
+
const PortalComponent = _Platform.default.OS === 'web' ? _Portal.default : _portal.Portal;
|
|
132
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(PortalComponent, {
|
|
127
133
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_View.default, {
|
|
128
134
|
ref: containerRef,
|
|
129
135
|
onLayout: onLayout,
|
|
@@ -6,7 +6,6 @@ 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");
|
|
10
9
|
var _View = _interopRequireDefault(require("react-native-web/dist/cjs/exports/View"));
|
|
11
10
|
var _StyleSheet = _interopRequireDefault(require("react-native-web/dist/cjs/exports/StyleSheet"));
|
|
12
11
|
var _Dimensions = _interopRequireDefault(require("react-native-web/dist/cjs/exports/Dimensions"));
|
|
@@ -219,20 +218,6 @@ const MultiSelectFilter = /*#__PURE__*/_react.default.forwardRef((_ref3, ref) =>
|
|
|
219
218
|
setIsOpen(false);
|
|
220
219
|
onCancel();
|
|
221
220
|
};
|
|
222
|
-
const appRootRef = _react.default.useRef(null);
|
|
223
|
-
const [rootOffsets, setRootOffsets] = _react.default.useState(null);
|
|
224
|
-
_react.default.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({
|
|
231
|
-
horizontal: x,
|
|
232
|
-
vertical: y
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
}, [isOpen, rootOffsets]);
|
|
236
221
|
const {
|
|
237
222
|
align,
|
|
238
223
|
offsets
|
|
@@ -249,8 +234,7 @@ const MultiSelectFilter = /*#__PURE__*/_react.default.forwardRef((_ref3, ref) =>
|
|
|
249
234
|
left: 'left'
|
|
250
235
|
},
|
|
251
236
|
offsets: {
|
|
252
|
-
vertical: 4
|
|
253
|
-
horizontal: -rootOffsets?.horizontal || 0
|
|
237
|
+
vertical: 4
|
|
254
238
|
}
|
|
255
239
|
}
|
|
256
240
|
});
|
|
@@ -387,12 +371,7 @@ const MultiSelectFilter = /*#__PURE__*/_react.default.forwardRef((_ref3, ref) =>
|
|
|
387
371
|
})]
|
|
388
372
|
});
|
|
389
373
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
|
|
390
|
-
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(
|
|
391
|
-
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_View.default, {
|
|
392
|
-
ref: appRootRef,
|
|
393
|
-
style: styles.appRootRef
|
|
394
|
-
})
|
|
395
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_Button.ButtonDropdown, {
|
|
374
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_Button.ButtonDropdown, {
|
|
396
375
|
ref: sourceRef,
|
|
397
376
|
...pressHandlers,
|
|
398
377
|
value: isOpen,
|
|
@@ -473,11 +452,6 @@ const styles = _StyleSheet.default.create({
|
|
|
473
452
|
},
|
|
474
453
|
scrollContainer: {
|
|
475
454
|
padding: 1
|
|
476
|
-
},
|
|
477
|
-
appRootRef: {
|
|
478
|
-
position: 'absolute',
|
|
479
|
-
top: 0,
|
|
480
|
-
left: 0
|
|
481
455
|
}
|
|
482
456
|
});
|
|
483
457
|
|
|
@@ -279,7 +279,8 @@ const TextInputBase = /*#__PURE__*/_react.default.forwardRef((_ref8, ref) => {
|
|
|
279
279
|
}, [element, pattern]);
|
|
280
280
|
const handleChangeText = event => {
|
|
281
281
|
const text = event.nativeEvent?.text ?? event.target?.value;
|
|
282
|
-
|
|
282
|
+
// Do NOT use `|| undefined`: empty string is falsy and would break the controlled contract
|
|
283
|
+
let filteredText = isNumeric ? text?.replace(/[^\d]/g, '') ?? '' : text;
|
|
283
284
|
if (type === 'card' && filteredText) {
|
|
284
285
|
const formattedValue = filteredText.replace(/[^a-zA-Z0-9]/g, '');
|
|
285
286
|
const regex = new RegExp(`([a-zA-Z0-9]{4})(?=[a-zA-Z0-9])`, 'g');
|
|
@@ -61,46 +61,45 @@ function getOverlaidPosition(_ref2) {
|
|
|
61
61
|
offsets = {},
|
|
62
62
|
align
|
|
63
63
|
} = _ref2;
|
|
64
|
-
// Web-only: this will be difficult to mimic on native because there's no global scroll position.
|
|
65
|
-
// TODO: wire something in e.g. a scroll ref accessible from a provider included in Allium provider
|
|
66
|
-
// that can be passed to the appropriate ScrollView?
|
|
67
|
-
const {
|
|
68
|
-
scrollX = 0,
|
|
69
|
-
scrollY = 0
|
|
70
|
-
} = typeof window === 'object' ? window : {};
|
|
71
|
-
|
|
72
64
|
// Will have top, bottom, left and/or right offsets depending on `align`
|
|
73
65
|
const positioning = {};
|
|
74
66
|
const verticalOffset = offsets.vertical ?? 0;
|
|
75
67
|
const horizontalOffset = offsets.horizontal ?? 0;
|
|
76
68
|
if (align.top) positioning.top = getPosition({
|
|
77
69
|
edge: getEdgeType(align, 'top'),
|
|
78
|
-
fromEdge: sourceLayout.y +
|
|
70
|
+
fromEdge: sourceLayout.y + verticalOffset,
|
|
79
71
|
sourceSize: sourceLayout.height
|
|
80
72
|
});
|
|
81
73
|
if (align.middle) positioning.top = getPosition({
|
|
82
74
|
edge: getEdgeType(align, 'middle'),
|
|
83
|
-
fromEdge: sourceLayout.y +
|
|
84
|
-
sourceSize: sourceLayout.height
|
|
85
|
-
});
|
|
86
|
-
if (align.bottom) positioning.bottom = getPosition({
|
|
87
|
-
edge: getEdgeType(align, 'bottom'),
|
|
88
|
-
fromEdge: windowDimensions.height - (sourceLayout.y + scrollY + sourceLayout.height - verticalOffset),
|
|
75
|
+
fromEdge: sourceLayout.y + verticalOffset - targetDimensions.height / 2,
|
|
89
76
|
sourceSize: sourceLayout.height
|
|
90
77
|
});
|
|
78
|
+
if (align.bottom) {
|
|
79
|
+
if (_Platform.default.OS !== 'web') {
|
|
80
|
+
// On native, position:absolute is parent-relative, so use negative top offset instead of window-based bottom math.
|
|
81
|
+
positioning.top = sourceLayout.y - targetDimensions.height - verticalOffset;
|
|
82
|
+
} else {
|
|
83
|
+
positioning.bottom = getPosition({
|
|
84
|
+
edge: getEdgeType(align, 'bottom'),
|
|
85
|
+
fromEdge: windowDimensions.height - (sourceLayout.y + sourceLayout.height - verticalOffset),
|
|
86
|
+
sourceSize: sourceLayout.height
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
91
90
|
if (align.left) positioning.left = getPosition({
|
|
92
91
|
edge: getEdgeType(align, 'left'),
|
|
93
|
-
fromEdge: sourceLayout.x +
|
|
92
|
+
fromEdge: sourceLayout.x + horizontalOffset,
|
|
94
93
|
sourceSize: sourceLayout.width
|
|
95
94
|
});
|
|
96
95
|
if (align.center) positioning.left = getPosition({
|
|
97
96
|
edge: getEdgeType(align, 'center'),
|
|
98
|
-
fromEdge: sourceLayout.x +
|
|
97
|
+
fromEdge: sourceLayout.x + horizontalOffset - targetDimensions.width / 2,
|
|
99
98
|
sourceSize: sourceLayout.width
|
|
100
99
|
});
|
|
101
100
|
if (align.right) positioning.right = getPosition({
|
|
102
101
|
edge: getEdgeType(align, 'right'),
|
|
103
|
-
fromEdge: windowDimensions.width - (sourceLayout.x +
|
|
102
|
+
fromEdge: windowDimensions.width - (sourceLayout.x + sourceLayout.width - horizontalOffset),
|
|
104
103
|
sourceSize: sourceLayout.width
|
|
105
104
|
});
|
|
106
105
|
if (!(align.left && align.right)) {
|
|
@@ -141,6 +140,32 @@ const useOverlaidPosition = _ref3 => {
|
|
|
141
140
|
const targetRef = (0, _react.useRef)(null);
|
|
142
141
|
const [targetDimensions, setTargetDimensions] = (0, _react.useState)(null);
|
|
143
142
|
const [windowDimensions, setWindowDimensions] = (0, _react.useState)(null);
|
|
143
|
+
const hasRemeasuredRef = (0, _react.useRef)(false);
|
|
144
|
+
const applySourceLayout = (0, _react.useCallback)((dims, x, y, width, height) => {
|
|
145
|
+
setWindowDimensions(dims);
|
|
146
|
+
setSourceLayout({
|
|
147
|
+
x,
|
|
148
|
+
y,
|
|
149
|
+
width,
|
|
150
|
+
height
|
|
151
|
+
});
|
|
152
|
+
}, []);
|
|
153
|
+
const measureSourceOnWeb = (0, _react.useCallback)(windowDims => {
|
|
154
|
+
const el = sourceRef.current;
|
|
155
|
+
if (!el) return;
|
|
156
|
+
const domNode = el._nativeTag ?? el;
|
|
157
|
+
const dims = windowDims ?? _Dimensions.default.get('window');
|
|
158
|
+
const rect = typeof domNode.getBoundingClientRect === 'function' ? domNode.getBoundingClientRect() : null;
|
|
159
|
+
if (rect) {
|
|
160
|
+
const x = rect.left + (typeof window.scrollX === 'number' ? window.scrollX : 0);
|
|
161
|
+
const y = rect.top + (typeof window.scrollY === 'number' ? window.scrollY : 0);
|
|
162
|
+
applySourceLayout(dims, x, y, rect.width, rect.height);
|
|
163
|
+
} else {
|
|
164
|
+
el.measureInWindow((mx, my, width, height) => {
|
|
165
|
+
applySourceLayout(dims, mx, my, width, height);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}, [applySourceLayout]);
|
|
144
169
|
const onTargetLayout = (0, _react.useCallback)(_ref4 => {
|
|
145
170
|
let {
|
|
146
171
|
nativeEvent: {
|
|
@@ -171,16 +196,19 @@ const useOverlaidPosition = _ref3 => {
|
|
|
171
196
|
let {
|
|
172
197
|
window
|
|
173
198
|
} = _ref5;
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
199
|
+
if (_Platform.default.OS === 'web') {
|
|
200
|
+
measureSourceOnWeb(window);
|
|
201
|
+
} else {
|
|
202
|
+
sourceRef.current?.measure((x, y, width, height) => {
|
|
203
|
+
setWindowDimensions(window);
|
|
204
|
+
setSourceLayout({
|
|
205
|
+
x,
|
|
206
|
+
y,
|
|
207
|
+
width,
|
|
208
|
+
height
|
|
209
|
+
});
|
|
182
210
|
});
|
|
183
|
-
}
|
|
211
|
+
}
|
|
184
212
|
};
|
|
185
213
|
let subscription;
|
|
186
214
|
const unsubscribe = () => {
|
|
@@ -193,6 +221,7 @@ const useOverlaidPosition = _ref3 => {
|
|
|
193
221
|
}
|
|
194
222
|
setSourceLayout(null);
|
|
195
223
|
setTargetDimensions(null);
|
|
224
|
+
hasRemeasuredRef.current = false;
|
|
196
225
|
};
|
|
197
226
|
if (readyToShow) {
|
|
198
227
|
subscription = _Dimensions.default.addEventListener('change', handleDimensionsChange);
|
|
@@ -203,36 +232,48 @@ const useOverlaidPosition = _ref3 => {
|
|
|
203
232
|
unsubscribe();
|
|
204
233
|
}
|
|
205
234
|
return unsubscribe;
|
|
206
|
-
}, [readyToShow]);
|
|
235
|
+
}, [readyToShow, measureSourceOnWeb]);
|
|
236
|
+
|
|
237
|
+
// Re-measure source when targetDimensions first becomes available.
|
|
238
|
+
// Without this, there is a race condition: getBoundingClientRect() resolves faster than
|
|
239
|
+
// measureInWindow used to, so sourceLayout may be set before the dropdown has rendered
|
|
240
|
+
// and reported its dimensions via onTargetLayout. When targetDimensions finally arrives,
|
|
241
|
+
// sourceLayout is stale (no scroll has occurred to trigger handleScroll).
|
|
242
|
+
// Setting hasRemeasuredRef.current=true here also prevents the blink: isReady (on web)
|
|
243
|
+
// won't become true until this effect completes, guaranteeing the dropdown is always shown
|
|
244
|
+
// at its final correct position without an intermediate incorrect-position frame.
|
|
245
|
+
(0, _react.useEffect)(() => {
|
|
246
|
+
if (!isShown || !sourceRef.current || !targetDimensions || _Platform.default.OS !== 'web') return;
|
|
247
|
+
measureSourceOnWeb();
|
|
248
|
+
hasRemeasuredRef.current = true;
|
|
249
|
+
}, [targetDimensions, isShown, measureSourceOnWeb]);
|
|
207
250
|
(0, _react.useEffect)(() => {
|
|
208
251
|
if (_Platform.default.OS !== 'web') {
|
|
209
252
|
return undefined;
|
|
210
253
|
}
|
|
211
|
-
const handleScroll = (0, _lodash.default)(
|
|
212
|
-
sourceRef.current?.measureInWindow((x, y, width, height) => {
|
|
213
|
-
setWindowDimensions(window);
|
|
214
|
-
setSourceLayout({
|
|
215
|
-
x,
|
|
216
|
-
y,
|
|
217
|
-
width,
|
|
218
|
-
height
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
}, DEBOUNCE_DELAY);
|
|
254
|
+
const handleScroll = (0, _lodash.default)(measureSourceOnWeb, DEBOUNCE_DELAY);
|
|
222
255
|
window.addEventListener('scroll', handleScroll);
|
|
223
256
|
return () => {
|
|
224
257
|
window.removeEventListener('scroll', handleScroll);
|
|
225
258
|
handleScroll.cancel();
|
|
226
259
|
};
|
|
227
|
-
}, [
|
|
228
|
-
|
|
260
|
+
}, [measureSourceOnWeb]);
|
|
261
|
+
|
|
262
|
+
// On web, require hasRemeasuredRef to be true before declaring isReady. This ensures
|
|
263
|
+
// the dropdown is never shown with an intermediate stale position (the blink).
|
|
264
|
+
// On native, hasRemeasuredRef stays false (the web-only effect never runs), so we skip
|
|
265
|
+
// that check to preserve the original native behaviour.
|
|
266
|
+
const isReady = Boolean(isShown && sourceLayout && windowDimensions && targetDimensions && (_Platform.default.OS !== 'web' || hasRemeasuredRef.current));
|
|
229
267
|
const overlaidPosition = isReady ? getOverlaidPosition({
|
|
230
268
|
sourceLayout,
|
|
231
269
|
targetDimensions,
|
|
232
270
|
windowDimensions,
|
|
233
271
|
offsets,
|
|
234
272
|
align
|
|
235
|
-
}) : {
|
|
273
|
+
}) : {
|
|
274
|
+
top: 0,
|
|
275
|
+
left: 0
|
|
276
|
+
};
|
|
236
277
|
return {
|
|
237
278
|
overlaidPosition,
|
|
238
279
|
sourceRef,
|
|
@@ -119,6 +119,7 @@ const Autocomplete = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
|
|
|
119
119
|
value,
|
|
120
120
|
helpText = '',
|
|
121
121
|
loadingLabel,
|
|
122
|
+
dropdownPosition = 'bottom',
|
|
122
123
|
tokens,
|
|
123
124
|
...rest
|
|
124
125
|
} = _ref2;
|
|
@@ -174,6 +175,13 @@ const Autocomplete = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
|
|
|
174
175
|
isShown: isExpanded || hintExpansionEnabled,
|
|
175
176
|
offsets: {
|
|
176
177
|
vertical: Platform.OS !== 'web' && (hint || inputLabel) ? 28 : 4
|
|
178
|
+
},
|
|
179
|
+
align: dropdownPosition === 'top' ? {
|
|
180
|
+
center: 'center',
|
|
181
|
+
bottom: 'top'
|
|
182
|
+
} : {
|
|
183
|
+
center: 'center',
|
|
184
|
+
top: 'bottom'
|
|
177
185
|
}
|
|
178
186
|
});
|
|
179
187
|
const targetRef = React.useRef(null);
|
|
@@ -526,6 +534,10 @@ Autocomplete.propTypes = {
|
|
|
526
534
|
/**
|
|
527
535
|
* Input value for controlled usage
|
|
528
536
|
*/
|
|
529
|
-
value: PropTypes.string
|
|
537
|
+
value: PropTypes.string,
|
|
538
|
+
/**
|
|
539
|
+
* Controls whether the dropdown renders above or below the input. Defaults to 'bottom'.
|
|
540
|
+
*/
|
|
541
|
+
dropdownPosition: PropTypes.oneOf(['top', 'bottom'])
|
|
530
542
|
};
|
|
531
543
|
export default Autocomplete;
|
|
@@ -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 =
|
|
3
|
+
export const DEFAULT_MAX_DROPDOWN_HEIGHT = 500;
|
|
4
4
|
export const INPUT_LEFT_PADDING = 16;
|
|
5
5
|
export const MIN_LISTBOX_WIDTH = 288;
|
|
@@ -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';
|
|
7
8
|
import { useThemeTokens } from '../ThemeProvider';
|
|
8
9
|
import Card from '../Card';
|
|
@@ -60,7 +61,12 @@ const DropdownOverlay = /*#__PURE__*/React.forwardRef((_ref, ref) => {
|
|
|
60
61
|
paddingLeft: paddingHorizontal,
|
|
61
62
|
paddingRight: paddingHorizontal
|
|
62
63
|
},
|
|
63
|
-
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 LocalPortal from '../Portal/Portal';
|
|
7
8
|
import { useCopy, copyPropTypes, getTokensPropType, variantProp } from '../utils';
|
|
8
9
|
import { useViewport } from '../ViewportProvider';
|
|
9
10
|
import { useThemeTokens } from '../ThemeProvider';
|
|
@@ -116,7 +117,12 @@ const ModalOverlay = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
|
|
|
116
117
|
copy
|
|
117
118
|
});
|
|
118
119
|
const closeLabel = getCopy('closeButton');
|
|
119
|
-
|
|
120
|
+
|
|
121
|
+
// On web, use the local Portal (always appends to document.body) to ensure correct
|
|
122
|
+
// absolute positioning in MFE/iframe contexts. @gorhom/portal may render its host
|
|
123
|
+
// outside the iframe's document, causing coordinate mismatches when scrolled.
|
|
124
|
+
const PortalComponent = Platform.OS === 'web' ? LocalPortal : GorhomPortal;
|
|
125
|
+
return /*#__PURE__*/_jsx(PortalComponent, {
|
|
120
126
|
children: /*#__PURE__*/_jsx(View, {
|
|
121
127
|
ref: containerRef,
|
|
122
128
|
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";
|
|
@@ -212,20 +211,6 @@ const MultiSelectFilter = /*#__PURE__*/React.forwardRef((_ref3, ref) => {
|
|
|
212
211
|
setIsOpen(false);
|
|
213
212
|
onCancel();
|
|
214
213
|
};
|
|
215
|
-
const appRootRef = React.useRef(null);
|
|
216
|
-
const [rootOffsets, setRootOffsets] = React.useState(null);
|
|
217
|
-
React.useEffect(() => {
|
|
218
|
-
if (rootOffsets) return;
|
|
219
|
-
appRootRef.current?.measureInWindow((x, y) => {
|
|
220
|
-
// Only set offsets if they are positive
|
|
221
|
-
// this is because we want to avoid negative offsets that could cause
|
|
222
|
-
// the dropdown to be positioned incorrectly in some situations
|
|
223
|
-
if (y > 0) setRootOffsets({
|
|
224
|
-
horizontal: x,
|
|
225
|
-
vertical: y
|
|
226
|
-
});
|
|
227
|
-
});
|
|
228
|
-
}, [isOpen, rootOffsets]);
|
|
229
214
|
const {
|
|
230
215
|
align,
|
|
231
216
|
offsets
|
|
@@ -242,8 +227,7 @@ const MultiSelectFilter = /*#__PURE__*/React.forwardRef((_ref3, ref) => {
|
|
|
242
227
|
left: 'left'
|
|
243
228
|
},
|
|
244
229
|
offsets: {
|
|
245
|
-
vertical: 4
|
|
246
|
-
horizontal: -rootOffsets?.horizontal || 0
|
|
230
|
+
vertical: 4
|
|
247
231
|
}
|
|
248
232
|
}
|
|
249
233
|
});
|
|
@@ -380,12 +364,7 @@ const MultiSelectFilter = /*#__PURE__*/React.forwardRef((_ref3, ref) => {
|
|
|
380
364
|
})]
|
|
381
365
|
});
|
|
382
366
|
return /*#__PURE__*/_jsxs(_Fragment, {
|
|
383
|
-
children: [/*#__PURE__*/_jsx(
|
|
384
|
-
children: /*#__PURE__*/_jsx(View, {
|
|
385
|
-
ref: appRootRef,
|
|
386
|
-
style: styles.appRootRef
|
|
387
|
-
})
|
|
388
|
-
}), /*#__PURE__*/_jsx(ButtonDropdown, {
|
|
367
|
+
children: [/*#__PURE__*/_jsx(ButtonDropdown, {
|
|
389
368
|
ref: sourceRef,
|
|
390
369
|
...pressHandlers,
|
|
391
370
|
value: isOpen,
|
|
@@ -466,11 +445,6 @@ const styles = StyleSheet.create({
|
|
|
466
445
|
},
|
|
467
446
|
scrollContainer: {
|
|
468
447
|
padding: 1
|
|
469
|
-
},
|
|
470
|
-
appRootRef: {
|
|
471
|
-
position: 'absolute',
|
|
472
|
-
top: 0,
|
|
473
|
-
left: 0
|
|
474
448
|
}
|
|
475
449
|
});
|
|
476
450
|
|
|
@@ -272,7 +272,8 @@ const TextInputBase = /*#__PURE__*/React.forwardRef((_ref8, ref) => {
|
|
|
272
272
|
}, [element, pattern]);
|
|
273
273
|
const handleChangeText = event => {
|
|
274
274
|
const text = event.nativeEvent?.text ?? event.target?.value;
|
|
275
|
-
|
|
275
|
+
// Do NOT use `|| undefined`: empty string is falsy and would break the controlled contract
|
|
276
|
+
let filteredText = isNumeric ? text?.replace(/[^\d]/g, '') ?? '' : text;
|
|
276
277
|
if (type === 'card' && filteredText) {
|
|
277
278
|
const formattedValue = filteredText.replace(/[^a-zA-Z0-9]/g, '');
|
|
278
279
|
const regex = new RegExp(`([a-zA-Z0-9]{4})(?=[a-zA-Z0-9])`, 'g');
|
|
@@ -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 +
|
|
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 +
|
|
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 +
|
|
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 +
|
|
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 +
|
|
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 @@ 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 @@ const useOverlaidPosition = _ref3 => {
|
|
|
164
189
|
let {
|
|
165
190
|
window
|
|
166
191
|
} = _ref5;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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 @@ 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 @@ 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
|
-
}, [
|
|
221
|
-
|
|
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
package/package.json
CHANGED
|
@@ -131,6 +131,7 @@ const Autocomplete = React.forwardRef(
|
|
|
131
131
|
value,
|
|
132
132
|
helpText = '',
|
|
133
133
|
loadingLabel,
|
|
134
|
+
dropdownPosition = 'bottom',
|
|
134
135
|
tokens,
|
|
135
136
|
...rest
|
|
136
137
|
},
|
|
@@ -182,7 +183,11 @@ const Autocomplete = React.forwardRef(
|
|
|
182
183
|
isShown: isExpanded || hintExpansionEnabled,
|
|
183
184
|
offsets: {
|
|
184
185
|
vertical: Platform.OS !== 'web' && (hint || inputLabel) ? 28 : 4
|
|
185
|
-
}
|
|
186
|
+
},
|
|
187
|
+
align:
|
|
188
|
+
dropdownPosition === 'top'
|
|
189
|
+
? { center: 'center', bottom: 'top' }
|
|
190
|
+
: { center: 'center', top: 'bottom' }
|
|
186
191
|
})
|
|
187
192
|
const targetRef = React.useRef(null)
|
|
188
193
|
// We limit the number of suggestions displayed to avoid huge lists
|
|
@@ -556,7 +561,11 @@ Autocomplete.propTypes = {
|
|
|
556
561
|
/**
|
|
557
562
|
* Input value for controlled usage
|
|
558
563
|
*/
|
|
559
|
-
value: PropTypes.string
|
|
564
|
+
value: PropTypes.string,
|
|
565
|
+
/**
|
|
566
|
+
* Controls whether the dropdown renders above or below the input. Defaults to 'bottom'.
|
|
567
|
+
*/
|
|
568
|
+
dropdownPosition: PropTypes.oneOf(['top', 'bottom'])
|
|
560
569
|
}
|
|
561
570
|
|
|
562
571
|
export default Autocomplete
|
|
@@ -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 =
|
|
3
|
+
export const DEFAULT_MAX_DROPDOWN_HEIGHT = 500
|
|
4
4
|
export const INPUT_LEFT_PADDING = 16
|
|
5
5
|
export const MIN_LISTBOX_WIDTH = 288
|
|
@@ -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
|
-
{
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
|
|
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')
|
|
@@ -54,11 +54,6 @@ function getOverlaidPosition({
|
|
|
54
54
|
offsets = {},
|
|
55
55
|
align
|
|
56
56
|
}) {
|
|
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 { scrollX = 0, scrollY = 0 } = typeof window === 'object' ? window : {}
|
|
61
|
-
|
|
62
57
|
// Will have top, bottom, left and/or right offsets depending on `align`
|
|
63
58
|
const positioning = {}
|
|
64
59
|
|
|
@@ -68,40 +63,44 @@ function getOverlaidPosition({
|
|
|
68
63
|
if (align.top)
|
|
69
64
|
positioning.top = getPosition({
|
|
70
65
|
edge: getEdgeType(align, 'top'),
|
|
71
|
-
fromEdge: sourceLayout.y +
|
|
66
|
+
fromEdge: sourceLayout.y + verticalOffset,
|
|
72
67
|
sourceSize: sourceLayout.height
|
|
73
68
|
})
|
|
74
69
|
if (align.middle)
|
|
75
70
|
positioning.top = getPosition({
|
|
76
71
|
edge: getEdgeType(align, 'middle'),
|
|
77
|
-
fromEdge: sourceLayout.y +
|
|
78
|
-
sourceSize: sourceLayout.height
|
|
79
|
-
})
|
|
80
|
-
if (align.bottom)
|
|
81
|
-
positioning.bottom = getPosition({
|
|
82
|
-
edge: getEdgeType(align, 'bottom'),
|
|
83
|
-
fromEdge:
|
|
84
|
-
windowDimensions.height - (sourceLayout.y + scrollY + sourceLayout.height - verticalOffset),
|
|
72
|
+
fromEdge: sourceLayout.y + verticalOffset - targetDimensions.height / 2,
|
|
85
73
|
sourceSize: sourceLayout.height
|
|
86
74
|
})
|
|
75
|
+
if (align.bottom) {
|
|
76
|
+
if (Platform.OS !== 'web') {
|
|
77
|
+
// On native, position:absolute is parent-relative, so use negative top offset instead of window-based bottom math.
|
|
78
|
+
positioning.top = sourceLayout.y - targetDimensions.height - verticalOffset
|
|
79
|
+
} else {
|
|
80
|
+
positioning.bottom = getPosition({
|
|
81
|
+
edge: getEdgeType(align, 'bottom'),
|
|
82
|
+
fromEdge: windowDimensions.height - (sourceLayout.y + sourceLayout.height - verticalOffset),
|
|
83
|
+
sourceSize: sourceLayout.height
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
87
|
|
|
88
88
|
if (align.left)
|
|
89
89
|
positioning.left = getPosition({
|
|
90
90
|
edge: getEdgeType(align, 'left'),
|
|
91
|
-
fromEdge: sourceLayout.x +
|
|
91
|
+
fromEdge: sourceLayout.x + horizontalOffset,
|
|
92
92
|
sourceSize: sourceLayout.width
|
|
93
93
|
})
|
|
94
94
|
if (align.center)
|
|
95
95
|
positioning.left = getPosition({
|
|
96
96
|
edge: getEdgeType(align, 'center'),
|
|
97
|
-
fromEdge: sourceLayout.x +
|
|
97
|
+
fromEdge: sourceLayout.x + horizontalOffset - targetDimensions.width / 2,
|
|
98
98
|
sourceSize: sourceLayout.width
|
|
99
99
|
})
|
|
100
100
|
if (align.right)
|
|
101
101
|
positioning.right = getPosition({
|
|
102
102
|
edge: getEdgeType(align, 'right'),
|
|
103
|
-
fromEdge:
|
|
104
|
-
windowDimensions.width - (sourceLayout.x + scrollX + sourceLayout.width - horizontalOffset),
|
|
103
|
+
fromEdge: windowDimensions.width - (sourceLayout.x + sourceLayout.width - horizontalOffset),
|
|
105
104
|
sourceSize: sourceLayout.width
|
|
106
105
|
})
|
|
107
106
|
|
|
@@ -149,6 +148,34 @@ const useOverlaidPosition = ({
|
|
|
149
148
|
|
|
150
149
|
const [windowDimensions, setWindowDimensions] = useState(null)
|
|
151
150
|
|
|
151
|
+
const hasRemeasuredRef = useRef(false)
|
|
152
|
+
|
|
153
|
+
const applySourceLayout = useCallback((dims, x, y, width, height) => {
|
|
154
|
+
setWindowDimensions(dims)
|
|
155
|
+
setSourceLayout({ x, y, width, height })
|
|
156
|
+
}, [])
|
|
157
|
+
|
|
158
|
+
const measureSourceOnWeb = useCallback(
|
|
159
|
+
(windowDims) => {
|
|
160
|
+
const el = sourceRef.current
|
|
161
|
+
if (!el) return
|
|
162
|
+
const domNode = el._nativeTag ?? el
|
|
163
|
+
const dims = windowDims ?? Dimensions.get('window')
|
|
164
|
+
const rect =
|
|
165
|
+
typeof domNode.getBoundingClientRect === 'function' ? domNode.getBoundingClientRect() : null
|
|
166
|
+
if (rect) {
|
|
167
|
+
const x = rect.left + (typeof window.scrollX === 'number' ? window.scrollX : 0)
|
|
168
|
+
const y = rect.top + (typeof window.scrollY === 'number' ? window.scrollY : 0)
|
|
169
|
+
applySourceLayout(dims, x, y, rect.width, rect.height)
|
|
170
|
+
} else {
|
|
171
|
+
el.measureInWindow((mx, my, width, height) => {
|
|
172
|
+
applySourceLayout(dims, mx, my, width, height)
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
[applySourceLayout]
|
|
177
|
+
)
|
|
178
|
+
|
|
152
179
|
const onTargetLayout = useCallback(
|
|
153
180
|
({
|
|
154
181
|
nativeEvent: {
|
|
@@ -173,12 +200,14 @@ const useOverlaidPosition = ({
|
|
|
173
200
|
const readyToShow = Boolean(isShown && sourceRef.current)
|
|
174
201
|
useEffect(() => {
|
|
175
202
|
const handleDimensionsChange = ({ window }) => {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
203
|
+
if (Platform.OS === 'web') {
|
|
204
|
+
measureSourceOnWeb(window)
|
|
205
|
+
} else {
|
|
206
|
+
sourceRef.current?.measure((x, y, width, height) => {
|
|
207
|
+
setWindowDimensions(window)
|
|
208
|
+
setSourceLayout({ x, y, width, height })
|
|
209
|
+
})
|
|
210
|
+
}
|
|
182
211
|
}
|
|
183
212
|
|
|
184
213
|
let subscription
|
|
@@ -192,6 +221,7 @@ const useOverlaidPosition = ({
|
|
|
192
221
|
}
|
|
193
222
|
setSourceLayout(null)
|
|
194
223
|
setTargetDimensions(null)
|
|
224
|
+
hasRemeasuredRef.current = false
|
|
195
225
|
}
|
|
196
226
|
|
|
197
227
|
if (readyToShow) {
|
|
@@ -202,19 +232,29 @@ const useOverlaidPosition = ({
|
|
|
202
232
|
}
|
|
203
233
|
|
|
204
234
|
return unsubscribe
|
|
205
|
-
}, [readyToShow])
|
|
235
|
+
}, [readyToShow, measureSourceOnWeb])
|
|
236
|
+
|
|
237
|
+
// Re-measure source when targetDimensions first becomes available.
|
|
238
|
+
// Without this, there is a race condition: getBoundingClientRect() resolves faster than
|
|
239
|
+
// measureInWindow used to, so sourceLayout may be set before the dropdown has rendered
|
|
240
|
+
// and reported its dimensions via onTargetLayout. When targetDimensions finally arrives,
|
|
241
|
+
// sourceLayout is stale (no scroll has occurred to trigger handleScroll).
|
|
242
|
+
// Setting hasRemeasuredRef.current=true here also prevents the blink: isReady (on web)
|
|
243
|
+
// won't become true until this effect completes, guaranteeing the dropdown is always shown
|
|
244
|
+
// at its final correct position without an intermediate incorrect-position frame.
|
|
245
|
+
useEffect(() => {
|
|
246
|
+
if (!isShown || !sourceRef.current || !targetDimensions || Platform.OS !== 'web') return
|
|
247
|
+
|
|
248
|
+
measureSourceOnWeb()
|
|
249
|
+
hasRemeasuredRef.current = true
|
|
250
|
+
}, [targetDimensions, isShown, measureSourceOnWeb])
|
|
206
251
|
|
|
207
252
|
useEffect(() => {
|
|
208
253
|
if (Platform.OS !== 'web') {
|
|
209
254
|
return undefined
|
|
210
255
|
}
|
|
211
256
|
|
|
212
|
-
const handleScroll = debounce(
|
|
213
|
-
sourceRef.current?.measureInWindow((x, y, width, height) => {
|
|
214
|
-
setWindowDimensions(window)
|
|
215
|
-
setSourceLayout({ x, y, width, height })
|
|
216
|
-
})
|
|
217
|
-
}, DEBOUNCE_DELAY)
|
|
257
|
+
const handleScroll = debounce(measureSourceOnWeb, DEBOUNCE_DELAY)
|
|
218
258
|
|
|
219
259
|
window.addEventListener('scroll', handleScroll)
|
|
220
260
|
|
|
@@ -222,9 +262,19 @@ const useOverlaidPosition = ({
|
|
|
222
262
|
window.removeEventListener('scroll', handleScroll)
|
|
223
263
|
handleScroll.cancel()
|
|
224
264
|
}
|
|
225
|
-
}, [
|
|
265
|
+
}, [measureSourceOnWeb])
|
|
226
266
|
|
|
227
|
-
|
|
267
|
+
// On web, require hasRemeasuredRef to be true before declaring isReady. This ensures
|
|
268
|
+
// the dropdown is never shown with an intermediate stale position (the blink).
|
|
269
|
+
// On native, hasRemeasuredRef stays false (the web-only effect never runs), so we skip
|
|
270
|
+
// that check to preserve the original native behaviour.
|
|
271
|
+
const isReady = Boolean(
|
|
272
|
+
isShown &&
|
|
273
|
+
sourceLayout &&
|
|
274
|
+
windowDimensions &&
|
|
275
|
+
targetDimensions &&
|
|
276
|
+
(Platform.OS !== 'web' || hasRemeasuredRef.current)
|
|
277
|
+
)
|
|
228
278
|
|
|
229
279
|
const overlaidPosition = isReady
|
|
230
280
|
? getOverlaidPosition({
|
|
@@ -234,7 +284,7 @@ const useOverlaidPosition = ({
|
|
|
234
284
|
offsets,
|
|
235
285
|
align
|
|
236
286
|
})
|
|
237
|
-
: {}
|
|
287
|
+
: { top: 0, left: 0 }
|
|
238
288
|
|
|
239
289
|
return {
|
|
240
290
|
overlaidPosition,
|