@telus-uds/components-base 4.0.0-alpha.1 → 4.0.0-beta.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 +43 -1
- package/lib/cjs/Autocomplete/Autocomplete.js +13 -1
- package/lib/cjs/Autocomplete/constants.js +1 -1
- package/lib/cjs/Footnote/FootnoteLink.js +18 -17
- 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/Progress/Progress.js +30 -5
- package/lib/cjs/Scroll/Scroll.js +472 -0
- package/lib/cjs/Scroll/ScrollContext.js +55 -0
- package/lib/cjs/Scroll/ScrollItem.js +106 -0
- package/lib/cjs/Scroll/ScrollProgress.js +100 -0
- package/lib/cjs/Scroll/dictionary.js +18 -0
- package/lib/cjs/TextInput/TextInputBase.js +2 -1
- package/lib/cjs/index.js +14 -0
- package/lib/cjs/utils/animation/useAnimatedValue.js +46 -0
- package/lib/cjs/utils/children.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/Footnote/FootnoteLink.js +18 -17
- 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/Progress/Progress.js +30 -5
- package/lib/esm/Scroll/Scroll.js +465 -0
- package/lib/esm/Scroll/ScrollContext.js +46 -0
- package/lib/esm/Scroll/ScrollItem.js +100 -0
- package/lib/esm/Scroll/ScrollProgress.js +93 -0
- package/lib/esm/Scroll/dictionary.js +12 -0
- package/lib/esm/TextInput/TextInputBase.js +2 -1
- package/lib/esm/index.js +2 -0
- package/lib/esm/utils/animation/useAnimatedValue.js +38 -0
- package/lib/esm/utils/children.js +2 -1
- package/lib/esm/utils/useOverlaidPosition.js +83 -42
- package/lib/package.json +2 -2
- package/package.json +2 -2
- package/src/Autocomplete/Autocomplete.jsx +11 -2
- package/src/Autocomplete/constants.js +1 -1
- package/src/Footnote/FootnoteLink.jsx +17 -12
- package/src/Listbox/ListboxOverlay.jsx +6 -2
- package/src/MultiSelectFilter/ModalOverlay.jsx +9 -3
- package/src/MultiSelectFilter/MultiSelectFilter.jsx +1 -31
- package/src/Progress/Progress.jsx +22 -3
- package/src/Scroll/Scroll.jsx +483 -0
- package/src/Scroll/ScrollContext.jsx +39 -0
- package/src/Scroll/ScrollItem.jsx +87 -0
- package/src/Scroll/ScrollProgress.jsx +74 -0
- package/src/Scroll/dictionary.js +12 -0
- package/src/TextInput/TextInputBase.jsx +2 -1
- package/src/index.js +2 -0
- package/src/utils/animation/useAnimatedValue.js +35 -0
- package/src/utils/children.jsx +4 -1
- package/src/utils/useOverlaidPosition.js +84 -34
- package/types/A11yInfoProvider.d.ts +9 -0
- package/types/A11yText.d.ts +13 -0
- package/types/ActionCard.d.ts +19 -0
- package/types/ActivityIndicator.d.ts +13 -0
- package/types/Autocomplete.d.ts +27 -0
- package/types/Badge.d.ts +15 -4
- package/types/BaseProvider.d.ts +26 -0
- package/types/Box.d.ts +13 -3
- package/types/Button.d.ts +35 -0
- package/types/Card.d.ts +19 -0
- package/types/CardGroup.d.ts +23 -0
- package/types/Carousel.d.ts +25 -0
- package/types/Checkbox.d.ts +15 -2
- package/types/CheckboxCard.d.ts +20 -0
- package/types/CheckboxCardGroup.d.ts +10 -4
- package/types/ChevronLink.d.ts +9 -2
- package/types/ColourToggle.d.ts +16 -0
- package/types/Common.d.ts +7 -3
- package/types/Divider.d.ts +13 -1
- package/types/DownloadApp.d.ts +19 -0
- package/types/ExpandCollapse.d.ts +16 -9
- package/types/ExpandCollapseMini.d.ts +16 -0
- package/types/Feedback.d.ts +16 -0
- package/types/Fieldset.d.ts +17 -0
- package/types/FileUpload.d.ts +6 -0
- package/types/FlexGrid.d.ts +57 -0
- package/types/Footnote.d.ts +18 -0
- package/types/HorizontalScroll.d.ts +15 -0
- package/types/HorizontalScrollButton.d.ts +17 -1
- package/types/Icon.d.ts +13 -2
- package/types/IconButton.d.ts +19 -0
- package/types/InputLabel.d.ts +12 -0
- package/types/InputSupports.d.ts +19 -0
- package/types/Link.d.ts +20 -4
- package/types/List.d.ts +11 -4
- package/types/Listbox.d.ts +13 -3
- package/types/Modal.d.ts +25 -0
- package/types/MultiSelectFilter.d.ts +24 -0
- package/types/Notification.d.ts +19 -0
- package/types/OrderedList.d.ts +17 -0
- package/types/Pagination.d.ts +25 -0
- package/types/Portal.d.ts +5 -1
- package/types/PriceLockup.d.ts +28 -0
- package/types/ProductCard.d.ts +20 -0
- package/types/ProductCardGroup.d.ts +21 -0
- package/types/Progress.d.ts +22 -0
- package/types/QuickLinks.d.ts +27 -0
- package/types/QuickLinksFeature.d.ts +27 -0
- package/types/Radio.d.ts +22 -0
- package/types/RadioCard.d.ts +21 -0
- package/types/Responsive.d.ts +14 -0
- package/types/Scroll.d.ts +66 -0
- package/types/Search.d.ts +15 -2
- package/types/Select.d.ts +12 -4
- package/types/SideNav.d.ts +38 -0
- package/types/Skeleton.d.ts +17 -0
- package/types/SkipLink.d.ts +14 -0
- package/types/Spacer.d.ts +8 -1
- package/types/StackView.d.ts +8 -2
- package/types/Status.d.ts +2 -3
- package/types/StepTracker.d.ts +16 -0
- package/types/TabBar.d.ts +25 -0
- package/types/Tabs.d.ts +12 -3
- package/types/Tags.d.ts +24 -0
- package/types/TextButton.d.ts +11 -4
- package/types/TextInput.d.ts +38 -0
- package/types/ThemeProvider.d.ts +29 -0
- package/types/Timeline.d.ts +16 -0
- package/types/ToggleSwitch.d.ts +11 -2
- package/types/ToolTip.d.ts +7 -3
- package/types/TooltipButton.d.ts +20 -0
- package/types/Typography.d.ts +8 -2
- package/types/Validator.d.ts +16 -0
- package/types/ViewportProvider.d.ts +12 -0
- package/types/index.d.ts +205 -49
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import View from "react-native-web/dist/exports/View";
|
|
4
|
+
import { resolvePressableState } from '../utils/pressability';
|
|
5
|
+
import { useAnimatedValue } from '../utils/animation/useAnimatedValue';
|
|
6
|
+
import { Progress } from '../Progress/Progress';
|
|
7
|
+
import { ProgressBar } from '../Progress/ProgressBar';
|
|
8
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
9
|
+
const MAX_PERCENTAGE = 100;
|
|
10
|
+
const BORDER_RADIUS_DIVISOR = 2;
|
|
11
|
+
const staticProgressTokens = {
|
|
12
|
+
borderWidth: 0,
|
|
13
|
+
borderColor: 'transparent'
|
|
14
|
+
};
|
|
15
|
+
const staticProgressBarTokens = {
|
|
16
|
+
gradient: null,
|
|
17
|
+
outlineWidth: 0
|
|
18
|
+
};
|
|
19
|
+
const selectProgressTokens = _ref => {
|
|
20
|
+
let {
|
|
21
|
+
progressTrackHeight,
|
|
22
|
+
progressBarHeight,
|
|
23
|
+
progressTrackColor
|
|
24
|
+
} = _ref;
|
|
25
|
+
return {
|
|
26
|
+
height: progressTrackHeight,
|
|
27
|
+
barHeight: progressBarHeight,
|
|
28
|
+
backgroundColor: progressTrackColor,
|
|
29
|
+
borderRadius: progressTrackHeight / BORDER_RADIUS_DIVISOR
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
const selectBarTokens = _ref2 => {
|
|
33
|
+
let {
|
|
34
|
+
progressBarColor,
|
|
35
|
+
progressBarHeight
|
|
36
|
+
} = _ref2;
|
|
37
|
+
return {
|
|
38
|
+
backgroundColor: progressBarColor,
|
|
39
|
+
borderRadius: progressBarHeight / BORDER_RADIUS_DIVISOR
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
export const ScrollProgress = /*#__PURE__*/React.forwardRef((_ref3, ref) => {
|
|
43
|
+
let {
|
|
44
|
+
pressableState,
|
|
45
|
+
getTokens,
|
|
46
|
+
thumbWrapperStyle,
|
|
47
|
+
getCopy
|
|
48
|
+
} = _ref3;
|
|
49
|
+
const state = resolvePressableState(pressableState);
|
|
50
|
+
const themeTokens = getTokens(state);
|
|
51
|
+
const animatedBarHeight = useAnimatedValue(themeTokens.progressBarHeight);
|
|
52
|
+
const animatedTokens = {
|
|
53
|
+
...themeTokens,
|
|
54
|
+
progressBarHeight: animatedBarHeight
|
|
55
|
+
};
|
|
56
|
+
return /*#__PURE__*/_jsx(Progress, {
|
|
57
|
+
tokens: {
|
|
58
|
+
...staticProgressTokens,
|
|
59
|
+
...selectProgressTokens(animatedTokens)
|
|
60
|
+
},
|
|
61
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
62
|
+
ref: ref,
|
|
63
|
+
style: thumbWrapperStyle,
|
|
64
|
+
children: /*#__PURE__*/_jsx(ProgressBar, {
|
|
65
|
+
percentage: MAX_PERCENTAGE,
|
|
66
|
+
tokens: {
|
|
67
|
+
...staticProgressBarTokens,
|
|
68
|
+
...selectBarTokens(animatedTokens)
|
|
69
|
+
},
|
|
70
|
+
accessibilityLabel: getCopy('progressBarLabel')
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
ScrollProgress.displayName = 'ScrollProgress';
|
|
76
|
+
ScrollProgress.propTypes = {
|
|
77
|
+
/**
|
|
78
|
+
* State object from Pressable's render callback containing hovered, pressed, and focused states
|
|
79
|
+
*/
|
|
80
|
+
pressableState: PropTypes.object.isRequired,
|
|
81
|
+
/**
|
|
82
|
+
* Theme token resolver callback from useThemeTokensCallback that accepts appearance state
|
|
83
|
+
*/
|
|
84
|
+
getTokens: PropTypes.func.isRequired,
|
|
85
|
+
/**
|
|
86
|
+
* Style array for the thumb wrapper containing static and dynamic position styles
|
|
87
|
+
*/
|
|
88
|
+
thumbWrapperStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.array]).isRequired,
|
|
89
|
+
/**
|
|
90
|
+
* Copy resolver function from useCopy for retrieving localized strings
|
|
91
|
+
*/
|
|
92
|
+
getCopy: PropTypes.func.isRequired
|
|
93
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const dictionary = {
|
|
2
|
+
en: {
|
|
3
|
+
accessibilityLabel: 'Scrollable content',
|
|
4
|
+
instructionText: 'Use Tab, arrow keys, or space bar to navigate',
|
|
5
|
+
progressBarLabel: 'Scroll position'
|
|
6
|
+
},
|
|
7
|
+
fr: {
|
|
8
|
+
accessibilityLabel: 'Contenu défilable',
|
|
9
|
+
instructionText: "Utilisez Tab, les touches fléchées ou la barre d'espace pour naviguer",
|
|
10
|
+
progressBarLabel: 'Position de défilement'
|
|
11
|
+
}
|
|
12
|
+
};
|
|
@@ -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
|
-
|
|
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');
|
package/lib/esm/index.js
CHANGED
|
@@ -73,6 +73,8 @@ export { RadioGroup } from './Radio/RadioGroup';
|
|
|
73
73
|
export { RadioCard } from './RadioCard/RadioCard';
|
|
74
74
|
export { RadioCardGroup } from './RadioCard/RadioCardGroup';
|
|
75
75
|
export { Responsive } from './Responsive/Responsive';
|
|
76
|
+
export { Scroll } from './Scroll/Scroll';
|
|
77
|
+
export { ScrollItem } from './Scroll/ScrollItem';
|
|
76
78
|
export { Search } from './Search/Search';
|
|
77
79
|
export { Select } from './Select/Select';
|
|
78
80
|
export { SelectItem } from './Select/SelectItem';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Animated from "react-native-web/dist/exports/Animated";
|
|
3
|
+
import Easing from "react-native-web/dist/exports/Easing";
|
|
4
|
+
const ANIMATION_DURATION = 200;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Smoothly animates a numeric value to a new target using a quadratic ease-in-out
|
|
8
|
+
* curve. Returns the current interpolated value as a plain number so
|
|
9
|
+
* consumers can use it in non-Animated style props.
|
|
10
|
+
*
|
|
11
|
+
* @param {number} targetValue - The value to animate towards.
|
|
12
|
+
* @param {number} [duration=ANIMATION_DURATION] - Animation duration in ms.
|
|
13
|
+
*/
|
|
14
|
+
export const useAnimatedValue = function (targetValue) {
|
|
15
|
+
let duration = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ANIMATION_DURATION;
|
|
16
|
+
const [currentValue, setCurrentValue] = React.useState(targetValue);
|
|
17
|
+
const animatedValue = React.useRef(new Animated.Value(targetValue)).current;
|
|
18
|
+
React.useEffect(() => {
|
|
19
|
+
const id = animatedValue.addListener(_ref => {
|
|
20
|
+
let {
|
|
21
|
+
value
|
|
22
|
+
} = _ref;
|
|
23
|
+
return setCurrentValue(value);
|
|
24
|
+
});
|
|
25
|
+
return () => animatedValue.removeListener(id);
|
|
26
|
+
}, [animatedValue]);
|
|
27
|
+
React.useEffect(() => {
|
|
28
|
+
const animation = Animated.timing(animatedValue, {
|
|
29
|
+
toValue: targetValue,
|
|
30
|
+
duration,
|
|
31
|
+
easing: Easing.inOut(Easing.quad),
|
|
32
|
+
useNativeDriver: false
|
|
33
|
+
});
|
|
34
|
+
animation.start();
|
|
35
|
+
return () => animation.stop();
|
|
36
|
+
}, [targetValue, animatedValue, duration]);
|
|
37
|
+
return currentValue;
|
|
38
|
+
};
|
|
@@ -47,7 +47,8 @@ export const unpackFragment = child => {
|
|
|
47
47
|
};
|
|
48
48
|
const isStringOrNumber = child => typeof child === 'string' || typeof child === 'number';
|
|
49
49
|
// Wrap an A11yText with neighouring text strings so it doesn't split them into multiple <Text>s
|
|
50
|
-
|
|
50
|
+
// Check displayName first as it is explicitly set and reliable regardless of export style (named vs default)
|
|
51
|
+
const isWrapable = child => isStringOrNumber(child) || child.type === A11yText || child.type?.__UDS_COMPONENT_NAME__ === 'FootnoteLink';
|
|
51
52
|
const combineKeys = childrenArray => childrenArray.reduce((newKey, child) => `${newKey}${child.key || ''}`, '');
|
|
52
53
|
|
|
53
54
|
// Group wrappable children for one `<Text>` parent, merging adjacent text nodes
|
|
@@ -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 @@ 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
|
-
|
|
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 @@ 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
|
-
}, [
|
|
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
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"@gorhom/portal": "^1.0.14",
|
|
13
13
|
"@react-native-picker/picker": "^2.11.4",
|
|
14
14
|
"@telus-uds/system-constants": "^4.0.0-alpha.0",
|
|
15
|
-
"@telus-uds/system-theme-tokens": "^5.0.0-alpha.
|
|
15
|
+
"@telus-uds/system-theme-tokens": "^5.0.0-alpha.2",
|
|
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-
|
|
84
|
+
"version": "4.0.0-beta.0",
|
|
85
85
|
"types": "types/index.d.ts"
|
|
86
86
|
}
|
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"@gorhom/portal": "^1.0.14",
|
|
13
13
|
"@react-native-picker/picker": "^2.11.4",
|
|
14
14
|
"@telus-uds/system-constants": "^4.0.0-alpha.0",
|
|
15
|
-
"@telus-uds/system-theme-tokens": "^5.0.0-alpha.
|
|
15
|
+
"@telus-uds/system-theme-tokens": "^5.0.0-alpha.2",
|
|
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-
|
|
84
|
+
"version": "4.0.0-beta.0",
|
|
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 =
|
|
3
|
+
export const DEFAULT_MAX_DROPDOWN_HEIGHT = 500
|
|
4
4
|
export const INPUT_LEFT_PADDING = 16
|
|
5
5
|
export const MIN_LISTBOX_WIDTH = 288
|
|
@@ -13,22 +13,26 @@ import { useThemeTokens } from '../ThemeProvider/useThemeTokens'
|
|
|
13
13
|
|
|
14
14
|
const [selectProps, selectedSystemPropTypes] = selectSystemProps([htmlAttrs, viewProps])
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
const HALF_FONT_SIZE = 2
|
|
17
|
+
const QUARTER_FONT_SIZE = 4
|
|
18
|
+
const DEFAULT_LINE_HEIGHT = 1
|
|
19
|
+
const LINE_HEIGHT_MULTIPLIER = 2
|
|
20
|
+
|
|
21
|
+
const selectTextStyle = ({ fontSize, lineHeight }) =>
|
|
22
|
+
Platform.select({
|
|
23
|
+
native: {
|
|
24
|
+
fontSize: fontSize / HALF_FONT_SIZE,
|
|
25
|
+
lineHeight: fontSize,
|
|
26
|
+
position: 'relative',
|
|
27
|
+
top: -(fontSize * lineHeight) / HALF_FONT_SIZE
|
|
26
28
|
},
|
|
27
29
|
web: {
|
|
28
|
-
|
|
30
|
+
fontSize,
|
|
31
|
+
lineHeight:
|
|
32
|
+
lineHeight !== DEFAULT_LINE_HEIGHT ? lineHeight : fontSize * LINE_HEIGHT_MULTIPLIER,
|
|
33
|
+
top: -fontSize / QUARTER_FONT_SIZE
|
|
29
34
|
}
|
|
30
35
|
})
|
|
31
|
-
})
|
|
32
36
|
|
|
33
37
|
export const FootnoteLink = React.forwardRef(
|
|
34
38
|
({ copy = 'en', content = [], onClick, tokens, variant = {}, ...rest }, ref) => {
|
|
@@ -69,6 +73,7 @@ export const FootnoteLink = React.forwardRef(
|
|
|
69
73
|
)
|
|
70
74
|
|
|
71
75
|
FootnoteLink.displayName = 'FootnoteLink'
|
|
76
|
+
FootnoteLink.__UDS_COMPONENT_NAME__ = 'FootnoteLink'
|
|
72
77
|
|
|
73
78
|
const copyShape = PropTypes.shape({ a11yLabel: PropTypes.string.isRequired })
|
|
74
79
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import PropTypes from 'prop-types'
|
|
3
|
-
import { View, StyleSheet, Platform } from 'react-native'
|
|
3
|
+
import { View, StyleSheet, Platform, ScrollView } from 'react-native'
|
|
4
4
|
import { Portal } from '../Portal/Portal'
|
|
5
5
|
import { useThemeTokens } from '../ThemeProvider/useThemeTokens'
|
|
6
6
|
import { Card } from '../Card/Card'
|
|
@@ -69,7 +69,11 @@ const DropdownOverlay = React.forwardRef(
|
|
|
69
69
|
paddingRight: paddingHorizontal
|
|
70
70
|
}}
|
|
71
71
|
>
|
|
72
|
-
{
|
|
72
|
+
{Platform.OS !== 'web' ? (
|
|
73
|
+
<ScrollView style={{ maxHeight }}>{children}</ScrollView>
|
|
74
|
+
) : (
|
|
75
|
+
children
|
|
76
|
+
)}
|
|
73
77
|
</Card>
|
|
74
78
|
</View>
|
|
75
79
|
)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import PropTypes from 'prop-types'
|
|
3
3
|
import { View, StyleSheet, Platform } from 'react-native'
|
|
4
|
-
import { Portal } from '@gorhom/portal'
|
|
4
|
+
import { Portal as GorhomPortal } from '@gorhom/portal'
|
|
5
|
+
import { Portal as LocalPortal } from '../Portal/Portal'
|
|
5
6
|
import { useCopy } from '../utils/useCopy'
|
|
6
7
|
import { copyPropTypes } from '../utils/props/copyPropTypes'
|
|
7
8
|
import { getTokensPropType } from '../utils/props/tokens'
|
|
@@ -107,8 +108,13 @@ export const ModalOverlay = React.forwardRef(
|
|
|
107
108
|
const getCopy = useCopy({ dictionary, copy })
|
|
108
109
|
const closeLabel = getCopy('closeButton')
|
|
109
110
|
|
|
111
|
+
// On web, use the local Portal (always appends to document.body) to ensure correct
|
|
112
|
+
// absolute positioning in MFE/iframe contexts. @gorhom/portal may render its host
|
|
113
|
+
// outside the iframe's document, causing coordinate mismatches when scrolled.
|
|
114
|
+
const PortalComponent = Platform.OS === 'web' ? LocalPortal : GorhomPortal
|
|
115
|
+
|
|
110
116
|
return (
|
|
111
|
-
<
|
|
117
|
+
<PortalComponent>
|
|
112
118
|
<View
|
|
113
119
|
ref={containerRef}
|
|
114
120
|
onLayout={onLayout}
|
|
@@ -137,7 +143,7 @@ export const ModalOverlay = React.forwardRef(
|
|
|
137
143
|
{children}
|
|
138
144
|
</Card>
|
|
139
145
|
</View>
|
|
140
|
-
</
|
|
146
|
+
</PortalComponent>
|
|
141
147
|
)
|
|
142
148
|
}
|
|
143
149
|
)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import PropTypes from 'prop-types'
|
|
3
|
-
import { Portal } from '@gorhom/portal'
|
|
4
3
|
import { View, StyleSheet, Dimensions, SafeAreaView, Platform, ScrollView } from 'react-native'
|
|
5
4
|
import { useThemeTokens, useThemeTokensCallback } from '../ThemeProvider/useThemeTokens'
|
|
6
5
|
import { applyTextStyles } from '../ThemeProvider/utils/styles'
|
|
@@ -218,27 +217,11 @@ export const MultiSelectFilter = React.forwardRef(
|
|
|
218
217
|
onCancel()
|
|
219
218
|
}
|
|
220
219
|
|
|
221
|
-
const appRootRef = React.useRef(null)
|
|
222
|
-
const [rootOffsets, setRootOffsets] = React.useState(null)
|
|
223
|
-
|
|
224
|
-
React.useEffect(() => {
|
|
225
|
-
if (rootOffsets) return
|
|
226
|
-
appRootRef.current?.measureInWindow((x, y) => {
|
|
227
|
-
// Only set offsets if they are positive
|
|
228
|
-
// this is because we want to avoid negative offsets that could cause
|
|
229
|
-
// the dropdown to be positioned incorrectly in some situations
|
|
230
|
-
if (y > 0) setRootOffsets({ horizontal: x, vertical: y })
|
|
231
|
-
})
|
|
232
|
-
}, [isOpen, rootOffsets])
|
|
233
|
-
|
|
234
220
|
const { align, offsets } = useResponsiveProp({
|
|
235
221
|
xs: { align: { top: 'top', left: 'left' } },
|
|
236
222
|
sm: {
|
|
237
223
|
align: { top: 'bottom', left: 'left' },
|
|
238
|
-
offsets: {
|
|
239
|
-
vertical: 4 - (rootOffsets?.vertical || 0),
|
|
240
|
-
horizontal: -rootOffsets?.horizontal || 0
|
|
241
|
-
}
|
|
224
|
+
offsets: { vertical: 4 }
|
|
242
225
|
}
|
|
243
226
|
})
|
|
244
227
|
|
|
@@ -364,14 +347,6 @@ export const MultiSelectFilter = React.forwardRef(
|
|
|
364
347
|
|
|
365
348
|
return (
|
|
366
349
|
<>
|
|
367
|
-
{/*
|
|
368
|
-
This View is rendered inside a Portal to determine the application's root position.
|
|
369
|
-
Capturing it is crucial to determine offsets to position the Modal correctly, preventing
|
|
370
|
-
misalignment that can occur when the Portal does not render the Modal at the body level.
|
|
371
|
-
*/}
|
|
372
|
-
<Portal>
|
|
373
|
-
<View ref={appRootRef} style={styles.appRootRef} />
|
|
374
|
-
</Portal>
|
|
375
350
|
<ButtonDropdown
|
|
376
351
|
ref={sourceRef}
|
|
377
352
|
key={id}
|
|
@@ -460,11 +435,6 @@ const styles = StyleSheet.create({
|
|
|
460
435
|
},
|
|
461
436
|
scrollContainer: {
|
|
462
437
|
padding: 1
|
|
463
|
-
},
|
|
464
|
-
appRootRef: {
|
|
465
|
-
position: 'absolute',
|
|
466
|
-
top: 0,
|
|
467
|
-
left: 0
|
|
468
438
|
}
|
|
469
439
|
})
|
|
470
440
|
|