@telus-uds/components-base 3.29.1 → 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 +29 -1
- package/lib/cjs/Autocomplete/Autocomplete.js +13 -1
- package/lib/cjs/Autocomplete/constants.js +1 -1
- package/lib/cjs/Carousel/Carousel.js +6 -1
- package/lib/cjs/Carousel/CarouselThumbnail.js +123 -31
- package/lib/cjs/Carousel/CarouselThumbnailNavigation.js +8 -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 +466 -0
- package/lib/cjs/Scroll/ScrollContext.js +55 -0
- package/lib/cjs/Scroll/ScrollItem.js +103 -0
- package/lib/cjs/Scroll/ScrollProgress.js +99 -0
- package/lib/cjs/Scroll/dictionary.js +18 -0
- package/lib/cjs/Scroll/index.js +29 -0
- package/lib/cjs/TextInput/TextInputBase.js +2 -1
- package/lib/cjs/index.js +15 -0
- package/lib/cjs/utils/animation/index.js +7 -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/Carousel/Carousel.js +6 -1
- package/lib/esm/Carousel/CarouselThumbnail.js +124 -32
- package/lib/esm/Carousel/CarouselThumbnailNavigation.js +8 -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 +459 -0
- package/lib/esm/Scroll/ScrollContext.js +47 -0
- package/lib/esm/Scroll/ScrollItem.js +96 -0
- package/lib/esm/Scroll/ScrollProgress.js +92 -0
- package/lib/esm/Scroll/dictionary.js +12 -0
- package/lib/esm/Scroll/index.js +4 -0
- package/lib/esm/TextInput/TextInputBase.js +2 -1
- package/lib/esm/index.js +1 -0
- package/lib/esm/utils/animation/index.js +1 -1
- package/lib/esm/utils/animation/useAnimatedValue.js +39 -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/Carousel/Carousel.jsx +6 -1
- package/src/Carousel/CarouselThumbnail.jsx +153 -64
- package/src/Carousel/CarouselThumbnailNavigation.jsx +8 -2
- 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 +488 -0
- package/src/Scroll/ScrollContext.jsx +41 -0
- package/src/Scroll/ScrollItem.jsx +89 -0
- package/src/Scroll/ScrollProgress.jsx +75 -0
- package/src/Scroll/dictionary.js +12 -0
- package/src/Scroll/index.js +5 -0
- package/src/TextInput/TextInputBase.jsx +2 -1
- package/src/index.js +1 -0
- package/src/utils/animation/index.js +1 -1
- package/src/utils/animation/useAnimatedValue.js +37 -0
- package/src/utils/children.jsx +4 -1
- package/src/utils/useOverlaidPosition.js +84 -34
- package/types/Scroll.d.ts +66 -0
- package/types/index.d.ts +9 -0
|
@@ -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');
|
package/lib/esm/index.js
CHANGED
|
@@ -45,6 +45,7 @@ export { default as QuickLinksFeature } from './QuickLinksFeature';
|
|
|
45
45
|
export { default as Radio, RadioGroup } from './Radio';
|
|
46
46
|
export { default as RadioCard, RadioCardGroup } from './RadioCard';
|
|
47
47
|
export { default as Responsive } from './Responsive';
|
|
48
|
+
export { default as Scroll, ScrollItem } from './Scroll';
|
|
48
49
|
export { default as Search } from './Search';
|
|
49
50
|
export { default as Select } from './Select';
|
|
50
51
|
export { default as Shortcuts, ShortcutsItem } from './Shortcuts';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
export { default as useAnimatedValue } from './useAnimatedValue';
|
|
2
2
|
export { default as useVerticalExpandAnimation } from './useVerticalExpandAnimation';
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
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
|
+
};
|
|
39
|
+
export default useAnimatedValue;
|
|
@@ -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 @@ 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
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"@gorhom/portal": "^1.0.14",
|
|
13
13
|
"@react-native-picker/picker": "^2.9.0",
|
|
14
14
|
"@telus-uds/system-constants": "^3.0.0",
|
|
15
|
-
"@telus-uds/system-theme-tokens": "^4.
|
|
15
|
+
"@telus-uds/system-theme-tokens": "^4.21.0",
|
|
16
16
|
"airbnb-prop-types": "^2.16.0",
|
|
17
17
|
"css-mediaquery": "^0.1.2",
|
|
18
18
|
"expo-document-picker": "^13.0.1",
|
|
@@ -84,6 +84,6 @@
|
|
|
84
84
|
"standard-engine": {
|
|
85
85
|
"skip": true
|
|
86
86
|
},
|
|
87
|
-
"version": "3.
|
|
87
|
+
"version": "3.31.0",
|
|
88
88
|
"types": "types/index.d.ts"
|
|
89
89
|
}
|
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"@gorhom/portal": "^1.0.14",
|
|
13
13
|
"@react-native-picker/picker": "^2.9.0",
|
|
14
14
|
"@telus-uds/system-constants": "^3.0.0",
|
|
15
|
-
"@telus-uds/system-theme-tokens": "^4.
|
|
15
|
+
"@telus-uds/system-theme-tokens": "^4.21.0",
|
|
16
16
|
"airbnb-prop-types": "^2.16.0",
|
|
17
17
|
"css-mediaquery": "^0.1.2",
|
|
18
18
|
"expo-document-picker": "^13.0.1",
|
|
@@ -84,6 +84,6 @@
|
|
|
84
84
|
"standard-engine": {
|
|
85
85
|
"skip": true
|
|
86
86
|
},
|
|
87
|
-
"version": "3.
|
|
87
|
+
"version": "3.31.0",
|
|
88
88
|
"types": "types/index.d.ts"
|
|
89
89
|
}
|
|
@@ -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
|
|
@@ -1403,9 +1403,14 @@ Carousel.propTypes = {
|
|
|
1403
1403
|
*/
|
|
1404
1404
|
thumbnails: PropTypes.arrayOf(
|
|
1405
1405
|
PropTypes.shape({
|
|
1406
|
+
/** Accessibility label for the thumbnail image, used by assistive technologies. */
|
|
1406
1407
|
accessibilityLabel: PropTypes.string,
|
|
1408
|
+
/** Alternative text for the thumbnail image, displayed when the image cannot be rendered. */
|
|
1407
1409
|
alt: PropTypes.string,
|
|
1408
|
-
|
|
1410
|
+
/** When true, renders a play icon overlay on the thumbnail to indicate that the slide contains a video. */
|
|
1411
|
+
isVideo: PropTypes.bool,
|
|
1412
|
+
/** URL or path of the thumbnail image. When used with `isVideo`, this should be the video's poster/preview image. */
|
|
1413
|
+
src: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
|
|
1409
1414
|
})
|
|
1410
1415
|
),
|
|
1411
1416
|
/**
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import PropTypes from 'prop-types'
|
|
3
|
-
import { Pressable, Image } from 'react-native'
|
|
3
|
+
import { StyleSheet, Pressable, Image, View } from 'react-native'
|
|
4
4
|
import { useCarousel } from './CarouselContext'
|
|
5
5
|
import { useThemeTokensCallback } from '../ThemeProvider'
|
|
6
6
|
import { useViewport } from '../ViewportProvider'
|
|
7
|
+
import Icon from '../Icon'
|
|
7
8
|
|
|
8
9
|
const selectPressableTokens = ({ borderColor, borderRadius, borderWidth, margin, padding }) => ({
|
|
9
10
|
borderColor,
|
|
@@ -13,83 +14,171 @@ const selectPressableTokens = ({ borderColor, borderRadius, borderWidth, margin,
|
|
|
13
14
|
padding
|
|
14
15
|
})
|
|
15
16
|
|
|
17
|
+
// Play icon overlay appearance is consistent across all brands and is not theme-configurable.
|
|
18
|
+
// The circle occupies 55% of the thumbnail size, and the icon occupies 55% of the circle diameter.
|
|
19
|
+
const PLAY_ICON_RATIO = 0.55
|
|
20
|
+
const PLAY_ICON_BORDER_RADIUS_DIVISOR = 2
|
|
21
|
+
|
|
22
|
+
const selectImageStyles = (size) => ({
|
|
23
|
+
width: size,
|
|
24
|
+
height: size
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const selectSelectedStyles = ({ selectedBorderColor, selectedBorderWidth, padding, margin }) => ({
|
|
28
|
+
borderColor: selectedBorderColor,
|
|
29
|
+
borderWidth: selectedBorderWidth,
|
|
30
|
+
padding: padding - selectedBorderWidth,
|
|
31
|
+
marginBottom: margin + selectedBorderWidth
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const selectNonSelectedStyles = ({ borderWidth, padding, margin, selectedBorderWidth }) => ({
|
|
35
|
+
padding: padding - borderWidth,
|
|
36
|
+
marginBottom: margin + selectedBorderWidth
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const selectPlayIconCircleStyles = (thumbnailSize) => {
|
|
40
|
+
const diameter = thumbnailSize * PLAY_ICON_RATIO
|
|
41
|
+
return {
|
|
42
|
+
width: diameter,
|
|
43
|
+
height: diameter,
|
|
44
|
+
borderRadius: diameter / PLAY_ICON_BORDER_RADIUS_DIVISOR
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const selectPlayIconTokens = (thumbnailSize) => ({
|
|
49
|
+
size: thumbnailSize * PLAY_ICON_RATIO * PLAY_ICON_RATIO
|
|
50
|
+
})
|
|
51
|
+
|
|
16
52
|
/**
|
|
17
53
|
* `Carousel.Thumbnail` is used to wrap the content of an individual slide and is suppsoed to be the
|
|
18
54
|
* only top-level component passed to the `Carousel`
|
|
19
55
|
*/
|
|
20
|
-
const CarouselThumbnail = React.forwardRef(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const { borderWidth, padding, selectedBorderColor, selectedBorderWidth, size, margin } =
|
|
36
|
-
getThumbnailTokens({ viewport })
|
|
37
|
-
const styles = {
|
|
38
|
-
image: {
|
|
39
|
-
height: size,
|
|
40
|
-
width: size
|
|
41
|
-
},
|
|
42
|
-
selected: {
|
|
43
|
-
borderColor: selectedBorderColor,
|
|
44
|
-
borderWidth: selectedBorderWidth,
|
|
45
|
-
padding: padding - selectedBorderWidth,
|
|
46
|
-
marginBottom: margin + selectedBorderWidth
|
|
47
|
-
},
|
|
48
|
-
nonSelected: {
|
|
49
|
-
padding: padding - borderWidth,
|
|
50
|
-
marginBottom: margin + selectedBorderWidth
|
|
56
|
+
const CarouselThumbnail = React.forwardRef(
|
|
57
|
+
({ accessibilityLabel, alt, index, isVideo, src }, ref) => {
|
|
58
|
+
const { activeIndex, itemLabel, totalItems, getCopyWithPlaceholders, goTo } = useCarousel()
|
|
59
|
+
const getThumbnailTokens = useThemeTokensCallback('CarouselThumbnail')
|
|
60
|
+
const viewport = useViewport()
|
|
61
|
+
const thumbnailTitle =
|
|
62
|
+
alt ??
|
|
63
|
+
getCopyWithPlaceholders('stepTrackerLabel')
|
|
64
|
+
.replace(/%\{itemLabel\}/g, itemLabel)
|
|
65
|
+
.replace(/%\{stepNumber\}/g, index)
|
|
66
|
+
.replace(/%\{stepCount\}/g, totalItems)
|
|
67
|
+
const handlePress = () => goTo(index)
|
|
68
|
+
const handleKeyDown = (event) => {
|
|
69
|
+
// Allow using the spacebar for navigation
|
|
70
|
+
if (event?.key === ' ') goTo(index)
|
|
51
71
|
}
|
|
52
|
-
|
|
72
|
+
const {
|
|
73
|
+
borderWidth,
|
|
74
|
+
padding,
|
|
75
|
+
selectedBorderColor,
|
|
76
|
+
selectedBorderWidth,
|
|
77
|
+
size,
|
|
78
|
+
margin,
|
|
79
|
+
playIcon
|
|
80
|
+
} = getThumbnailTokens({ viewport })
|
|
53
81
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
82
|
+
return (
|
|
83
|
+
<Pressable
|
|
84
|
+
key={src}
|
|
85
|
+
onKeyDown={handleKeyDown}
|
|
86
|
+
onPress={handlePress}
|
|
87
|
+
style={({ hovered, pressed, focused }) => {
|
|
88
|
+
const pressableStyles = selectPressableTokens(
|
|
89
|
+
getThumbnailTokens({
|
|
90
|
+
hover: hovered,
|
|
91
|
+
pressed,
|
|
92
|
+
focus: focused
|
|
93
|
+
})
|
|
94
|
+
)
|
|
67
95
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
96
|
+
return [
|
|
97
|
+
pressableStyles,
|
|
98
|
+
index === activeIndex
|
|
99
|
+
? [
|
|
100
|
+
selectSelectedStyles({
|
|
101
|
+
selectedBorderColor,
|
|
102
|
+
selectedBorderWidth,
|
|
103
|
+
padding,
|
|
104
|
+
margin
|
|
105
|
+
}),
|
|
106
|
+
staticStyles.selectedPressableOutline
|
|
107
|
+
]
|
|
108
|
+
: selectNonSelectedStyles({ borderWidth, padding, margin, selectedBorderWidth })
|
|
109
|
+
]
|
|
110
|
+
}}
|
|
111
|
+
ref={ref}
|
|
112
|
+
>
|
|
113
|
+
<View style={[staticStyles.imageContainer, selectImageStyles(size)]}>
|
|
114
|
+
<Image
|
|
115
|
+
accessibilityIgnoresInvertColors
|
|
116
|
+
accessibilityLabel={accessibilityLabel ?? alt}
|
|
117
|
+
source={src}
|
|
118
|
+
style={selectImageStyles(size)}
|
|
119
|
+
title={thumbnailTitle}
|
|
120
|
+
/>
|
|
121
|
+
{isVideo && playIcon && (
|
|
122
|
+
<View
|
|
123
|
+
style={staticStyles.playIconOverlayContainer}
|
|
124
|
+
testID={`play-icon-overlay-${index}`}
|
|
125
|
+
>
|
|
126
|
+
<View
|
|
127
|
+
style={[selectPlayIconCircleStyles(size), staticStyles.playIconCircleBackground]}
|
|
128
|
+
>
|
|
129
|
+
<Icon
|
|
130
|
+
icon={playIcon}
|
|
131
|
+
tokens={{
|
|
132
|
+
...selectPlayIconTokens(size),
|
|
133
|
+
color: staticStyles.playIconSymbol.color
|
|
134
|
+
}}
|
|
135
|
+
/>
|
|
136
|
+
</View>
|
|
137
|
+
</View>
|
|
138
|
+
)}
|
|
139
|
+
</View>
|
|
140
|
+
</Pressable>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
)
|
|
85
144
|
|
|
86
145
|
CarouselThumbnail.displayName = 'CarouselThumbnail'
|
|
87
146
|
|
|
88
147
|
CarouselThumbnail.propTypes = {
|
|
148
|
+
/** Accessibility label for screen readers, overrides the alt text */
|
|
89
149
|
accessibilityLabel: PropTypes.string,
|
|
150
|
+
/** Alt text for the thumbnail image */
|
|
90
151
|
alt: PropTypes.string,
|
|
152
|
+
/** Zero-based index of this thumbnail within the carousel */
|
|
91
153
|
index: PropTypes.number,
|
|
92
|
-
|
|
154
|
+
/**
|
|
155
|
+
* When true, renders a play icon overlay on the thumbnail to indicate that the slide contains a video.
|
|
156
|
+
*/
|
|
157
|
+
isVideo: PropTypes.bool,
|
|
158
|
+
/** Image source URI (web) or local asset require() (native) */
|
|
159
|
+
src: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
|
|
93
160
|
}
|
|
94
161
|
|
|
162
|
+
const staticStyles = StyleSheet.create({
|
|
163
|
+
imageContainer: {
|
|
164
|
+
position: 'relative'
|
|
165
|
+
},
|
|
166
|
+
playIconOverlayContainer: {
|
|
167
|
+
position: 'absolute',
|
|
168
|
+
top: 0,
|
|
169
|
+
left: 0,
|
|
170
|
+
right: 0,
|
|
171
|
+
bottom: 0,
|
|
172
|
+
justifyContent: 'center',
|
|
173
|
+
alignItems: 'center'
|
|
174
|
+
},
|
|
175
|
+
selectedPressableOutline: { outlineStyle: 'none' },
|
|
176
|
+
playIconCircleBackground: {
|
|
177
|
+
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
|
178
|
+
justifyContent: 'center',
|
|
179
|
+
alignItems: 'center'
|
|
180
|
+
},
|
|
181
|
+
playIconSymbol: { color: 'rgb(255, 255, 255)' }
|
|
182
|
+
})
|
|
183
|
+
|
|
95
184
|
export default CarouselThumbnail
|
|
@@ -27,11 +27,12 @@ const CarouselThumbnailNavigation = React.forwardRef(({ thumbnails = [] }, ref)
|
|
|
27
27
|
return (
|
|
28
28
|
<View style={containerStyles}>
|
|
29
29
|
<StackWrap direction="row" tokens={stackWrapTokens} space={2} ref={ref}>
|
|
30
|
-
{thumbnails.map(({ accessibilityLabel, alt, src }, index) => (
|
|
30
|
+
{thumbnails.map(({ accessibilityLabel, alt, isVideo, src }, index) => (
|
|
31
31
|
<CarouselThumbnail
|
|
32
32
|
accessibilityLabel={accessibilityLabel}
|
|
33
33
|
alt={alt}
|
|
34
34
|
index={index}
|
|
35
|
+
isVideo={isVideo}
|
|
35
36
|
key={src}
|
|
36
37
|
src={src}
|
|
37
38
|
/>
|
|
@@ -47,9 +48,14 @@ CarouselThumbnailNavigation.propTypes = {
|
|
|
47
48
|
*/
|
|
48
49
|
thumbnails: PropTypes.arrayOf(
|
|
49
50
|
PropTypes.shape({
|
|
51
|
+
/** Accessibility label for the thumbnail image, used by assistive technologies. */
|
|
50
52
|
accessibilityLabel: PropTypes.string,
|
|
53
|
+
/** Alternative text for the thumbnail image, displayed when the image cannot be rendered. */
|
|
51
54
|
alt: PropTypes.string,
|
|
52
|
-
|
|
55
|
+
/** When true, renders a play icon overlay on the thumbnail to indicate that the slide contains a video. */
|
|
56
|
+
isVideo: PropTypes.bool,
|
|
57
|
+
/** URL or path of the thumbnail image. When used with `isVideo`, this should be the video's poster/preview image. */
|
|
58
|
+
src: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
|
|
53
59
|
})
|
|
54
60
|
).isRequired
|
|
55
61
|
}
|