@utilitywarehouse/hearth-react-native 0.27.1 → 0.27.2-testid-fix-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.storybook/vitest.setup.ts +35 -3
- package/.turbo/turbo-build.log +5 -4
- package/CHANGELOG.md +15 -0
- package/build/components/Button/ButtonRoot.js +8 -0
- package/build/components/Carousel/Carousel.js +6 -1
- package/build/components/DatePicker/TimePicker.d.ts +3 -0
- package/build/components/DatePicker/TimePicker.js +84 -0
- package/build/components/DatePicker/time-picker/animated-math.d.ts +4 -0
- package/build/components/DatePicker/time-picker/animated-math.js +19 -0
- package/build/components/DatePicker/time-picker/period-native.d.ts +6 -0
- package/build/components/DatePicker/time-picker/period-native.js +17 -0
- package/build/components/DatePicker/time-picker/period-picker.d.ts +6 -0
- package/build/components/DatePicker/time-picker/period-picker.js +10 -0
- package/build/components/DatePicker/time-picker/period-web.d.ts +6 -0
- package/build/components/DatePicker/time-picker/period-web.js +21 -0
- package/build/components/DatePicker/time-picker/wheel-native.d.ts +8 -0
- package/build/components/DatePicker/time-picker/wheel-native.js +19 -0
- package/build/components/DatePicker/time-picker/wheel-picker/index.d.ts +2 -0
- package/build/components/DatePicker/time-picker/wheel-picker/index.js +2 -0
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.d.ts +16 -0
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.js +97 -0
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.d.ts +21 -0
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.js +88 -0
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.d.ts +23 -0
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.js +21 -0
- package/build/components/DatePicker/time-picker/wheel-web.d.ts +8 -0
- package/build/components/DatePicker/time-picker/wheel-web.js +146 -0
- package/build/components/DatePicker/time-picker/wheel.d.ts +8 -0
- package/build/components/DatePicker/time-picker/wheel.js +10 -0
- package/build/components/List/List.js +2 -2
- package/build/components/Modal/Modal.js +16 -11
- package/build/components/SegmentedControl/SegmentedControl.js +4 -1
- package/build/components/SegmentedControl/SegmentedControlOption.js +4 -1
- package/build/components/TimePicker/TimePickerWheel.js +9 -1
- package/build/components/Toast/Toast.context.js +1 -1
- package/build/components/VerificationInput/VerificationInput.js +11 -22
- package/build/components/VerificationInput/VerificationInput.utils.d.ts +8 -0
- package/build/components/VerificationInput/VerificationInput.utils.js +17 -0
- package/build/components/VerificationInput/VerificationInput.utils.test.d.ts +1 -0
- package/build/components/VerificationInput/VerificationInput.utils.test.js +36 -0
- package/docs/changelog.mdx +113 -0
- package/package.json +5 -4
- package/src/components/Button/Button.stories.tsx +43 -7
- package/src/components/Button/ButtonRoot.tsx +8 -0
- package/src/components/Carousel/Carousel.tsx +6 -2
- package/src/components/IconContainer/IconContainer.stories.tsx +35 -30
- package/src/components/List/List.tsx +5 -4
- package/src/components/Modal/Modal.tsx +31 -16
- package/src/components/SegmentedControl/SegmentedControl.tsx +4 -1
- package/src/components/SegmentedControl/SegmentedControlOption.tsx +5 -4
- package/src/components/TimePicker/TimePickerWheel.tsx +11 -4
- package/src/components/Toast/Toast.context.tsx +1 -1
- package/src/components/VerificationInput/VerificationInput.stories.tsx +33 -0
- package/src/components/VerificationInput/VerificationInput.tsx +18 -29
- package/src/components/VerificationInput/VerificationInput.utils.test.ts +48 -0
- package/src/components/VerificationInput/VerificationInput.utils.ts +32 -0
- package/tsconfig.eslint.json +2 -1
- package/vitest.config.js +11 -13
- package/vitest.unit.config.ts +9 -0
- package/.turbo/turbo-lint.log +0 -72
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { memo, useMemo, useRef } from 'react';
|
|
3
|
+
import { Animated, PanResponder, Platform, View } from 'react-native';
|
|
4
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
5
|
+
import { isEqual } from '../../../utils';
|
|
6
|
+
import { BodyText } from '../../BodyText';
|
|
7
|
+
import { CONTAINER_HEIGHT } from '../enums';
|
|
8
|
+
import { sin } from './animated-math';
|
|
9
|
+
const ITEM_HEIGHT = 44;
|
|
10
|
+
const WheelWeb = ({ value, setValue = () => { }, items }) => {
|
|
11
|
+
const displayCount = 5;
|
|
12
|
+
const translateY = useRef(new Animated.Value(0)).current;
|
|
13
|
+
const renderCount = displayCount * 2 < items.length ? displayCount * 8 : displayCount * 2 - 1;
|
|
14
|
+
const circular = items.length >= displayCount;
|
|
15
|
+
const height = 140;
|
|
16
|
+
const radius = height / 2;
|
|
17
|
+
const valueIndex = useMemo(() => {
|
|
18
|
+
return Math.max(0, items.findIndex(item => item.value === value));
|
|
19
|
+
}, [items, value]);
|
|
20
|
+
const panResponder = useMemo(() => {
|
|
21
|
+
return PanResponder.create({
|
|
22
|
+
onMoveShouldSetPanResponder: () => true,
|
|
23
|
+
onStartShouldSetPanResponderCapture: () => true,
|
|
24
|
+
onPanResponderGrant: () => {
|
|
25
|
+
translateY.setValue(0);
|
|
26
|
+
},
|
|
27
|
+
onPanResponderMove: (evt, gestureState) => {
|
|
28
|
+
translateY.setValue(gestureState.dy);
|
|
29
|
+
evt.stopPropagation();
|
|
30
|
+
},
|
|
31
|
+
onPanResponderRelease: (_, gestureState) => {
|
|
32
|
+
translateY.extractOffset();
|
|
33
|
+
let newValueIndex = valueIndex - Math.round(gestureState.dy / ((radius * 2) / displayCount));
|
|
34
|
+
if (circular) {
|
|
35
|
+
newValueIndex = (newValueIndex + items.length) % items.length;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
if (newValueIndex < 0) {
|
|
39
|
+
newValueIndex = 0;
|
|
40
|
+
}
|
|
41
|
+
else if (newValueIndex >= items.length) {
|
|
42
|
+
newValueIndex = items.length - 1;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const newValue = items[newValueIndex];
|
|
46
|
+
if (newValue?.value === value) {
|
|
47
|
+
translateY.setOffset(0);
|
|
48
|
+
translateY.setValue(0);
|
|
49
|
+
}
|
|
50
|
+
else if (newValue?.value) {
|
|
51
|
+
setValue(newValue.value);
|
|
52
|
+
}
|
|
53
|
+
else if (items[0]?.value) {
|
|
54
|
+
setValue(items[0].value);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}, [circular, displayCount, radius, setValue, value, valueIndex, items, translateY]);
|
|
59
|
+
const displayValues = useMemo(() => {
|
|
60
|
+
const centerIndex = Math.floor(renderCount / 2);
|
|
61
|
+
return Array.from({ length: renderCount }, (_, index) => {
|
|
62
|
+
let targetIndex = valueIndex + index - centerIndex;
|
|
63
|
+
if (circular) {
|
|
64
|
+
targetIndex = ((targetIndex % items.length) + items.length) % items.length;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
targetIndex = Math.max(0, Math.min(targetIndex, items.length - 1));
|
|
68
|
+
}
|
|
69
|
+
return items[targetIndex] || items[0];
|
|
70
|
+
});
|
|
71
|
+
}, [renderCount, valueIndex, items, circular]);
|
|
72
|
+
const animatedAngles = useMemo(() => {
|
|
73
|
+
//translateY.setValue(0);
|
|
74
|
+
translateY.setOffset(0);
|
|
75
|
+
const currentIndex = displayValues.findIndex(item => item?.value === value);
|
|
76
|
+
return displayValues && displayValues.length > 0
|
|
77
|
+
? displayValues.map((_, index) => translateY
|
|
78
|
+
.interpolate({
|
|
79
|
+
inputRange: [-radius, radius],
|
|
80
|
+
outputRange: [
|
|
81
|
+
-radius + ((radius * 2) / displayCount) * (index - currentIndex),
|
|
82
|
+
radius + ((radius * 2) / displayCount) * (index - currentIndex),
|
|
83
|
+
],
|
|
84
|
+
extrapolate: 'extend',
|
|
85
|
+
})
|
|
86
|
+
.interpolate({
|
|
87
|
+
inputRange: [-radius, radius],
|
|
88
|
+
outputRange: [-Math.PI / 2, Math.PI / 2],
|
|
89
|
+
extrapolate: 'clamp',
|
|
90
|
+
}))
|
|
91
|
+
: [];
|
|
92
|
+
}, [displayValues, radius, value, displayCount, translateY]);
|
|
93
|
+
return (_jsxs(View, { style: [styles.container], ...panResponder.panHandlers, children: [_jsx(View, { style: [
|
|
94
|
+
styles.selectedIndicator,
|
|
95
|
+
{
|
|
96
|
+
transform: [{ translateY: -ITEM_HEIGHT / 2 }],
|
|
97
|
+
height: ITEM_HEIGHT,
|
|
98
|
+
},
|
|
99
|
+
] }), displayValues?.map((displayValue, index) => {
|
|
100
|
+
const animatedAngle = animatedAngles[index];
|
|
101
|
+
return (_jsx(Animated.View, { style: {
|
|
102
|
+
position: 'absolute',
|
|
103
|
+
height: ITEM_HEIGHT - 10,
|
|
104
|
+
transform: animatedAngle
|
|
105
|
+
? [
|
|
106
|
+
{
|
|
107
|
+
translateY: Animated.multiply(radius, sin(animatedAngle)),
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
rotateX: animatedAngle.interpolate({
|
|
111
|
+
inputRange: [-Math.PI / 2, Math.PI / 2],
|
|
112
|
+
outputRange: ['-89deg', '89deg'],
|
|
113
|
+
extrapolate: 'clamp',
|
|
114
|
+
}),
|
|
115
|
+
},
|
|
116
|
+
]
|
|
117
|
+
: [],
|
|
118
|
+
opacity: displayValue?.value !== value ? 0.3 : 1,
|
|
119
|
+
}, children: _jsx(BodyText, { children: displayValue?.text }) }, `${displayValue?.text}-${index}`));
|
|
120
|
+
})] }));
|
|
121
|
+
};
|
|
122
|
+
const styles = StyleSheet.create({
|
|
123
|
+
container: {
|
|
124
|
+
minWidth: 30,
|
|
125
|
+
overflow: 'hidden',
|
|
126
|
+
alignItems: 'center',
|
|
127
|
+
justifyContent: 'center',
|
|
128
|
+
height: CONTAINER_HEIGHT / 2,
|
|
129
|
+
...Platform.select({
|
|
130
|
+
web: {
|
|
131
|
+
cursor: 'pointer',
|
|
132
|
+
userSelect: 'none',
|
|
133
|
+
},
|
|
134
|
+
}),
|
|
135
|
+
},
|
|
136
|
+
selectedIndicator: {
|
|
137
|
+
position: 'absolute',
|
|
138
|
+
width: '100%',
|
|
139
|
+
top: '50%',
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
const customComparator = (prev, next) => {
|
|
143
|
+
const areEqual = prev.value === next.value && prev.setValue === next.setValue && isEqual(prev.items, next.items);
|
|
144
|
+
return areEqual;
|
|
145
|
+
};
|
|
146
|
+
export default memo(WheelWeb, customComparator);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { PickerOption } from '../DatePicker.props';
|
|
2
|
+
type WheelProps = {
|
|
3
|
+
value: number | string;
|
|
4
|
+
setValue?: (value: any) => void;
|
|
5
|
+
items: PickerOption[];
|
|
6
|
+
};
|
|
7
|
+
declare const _default: import("react").MemoExoticComponent<(props: WheelProps) => import("react/jsx-runtime").JSX.Element>;
|
|
8
|
+
export default _default;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { memo } from 'react';
|
|
3
|
+
import { Platform } from 'react-native';
|
|
4
|
+
import WheelNative from './wheel-native';
|
|
5
|
+
import WheelWeb from './wheel-web';
|
|
6
|
+
const Wheel = (props) => {
|
|
7
|
+
const Component = Platform.OS === 'web' ? WheelWeb : WheelNative;
|
|
8
|
+
return _jsx(Component, { ...props });
|
|
9
|
+
};
|
|
10
|
+
export default memo(Wheel);
|
|
@@ -6,7 +6,7 @@ import { Card } from '../Card';
|
|
|
6
6
|
import { SectionHeader } from '../SectionHeader';
|
|
7
7
|
import { ListContext } from './List.context';
|
|
8
8
|
const List = ({ children, heading, helperText, headerTrailingContent, invalidText, ...props }) => {
|
|
9
|
-
const { loading, disabled, container = 'none' } = props;
|
|
9
|
+
const { loading, disabled, container = 'none', testID, style, ...rest } = props;
|
|
10
10
|
const orderRef = useRef([]);
|
|
11
11
|
const [firstItemId, setFirstItemId] = useState(undefined);
|
|
12
12
|
const containerToCard = {
|
|
@@ -35,7 +35,7 @@ const List = ({ children, heading, helperText, headerTrailingContent, invalidTex
|
|
|
35
35
|
registerItem,
|
|
36
36
|
};
|
|
37
37
|
styles.useVariants({ disabled });
|
|
38
|
-
return (_jsx(ListContext.Provider, { value: value, children: _jsxs(View, { ...
|
|
38
|
+
return (_jsx(ListContext.Provider, { value: value, children: _jsxs(View, { ...rest, style: [styles.container, style], children: [heading ? (_jsx(SectionHeader, { heading: heading, helperText: helperText, trailingContent: headerTrailingContent, invalidText: invalidText })) : null, container === 'none' ? (_jsx(View, { testID: testID, children: children })) : (React.Children.count(children) > 0 && (_jsx(Card, { ...containerToCard, noPadding: true, style: styles.card, testID: testID, children: _jsx(_Fragment, { children: children }) })))] }) }));
|
|
39
39
|
};
|
|
40
40
|
List.displayName = 'List';
|
|
41
41
|
const styles = StyleSheet.create(theme => ({
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { BottomSheetFooter, } from '@gorhom/bottom-sheet';
|
|
3
3
|
import { CloseMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
|
|
4
4
|
import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
5
|
-
import { AccessibilityInfo, Dimensions, Platform, ScrollView, View, findNodeHandle } from 'react-native';
|
|
5
|
+
import { AccessibilityInfo, Dimensions, Platform, ScrollView, View, findNodeHandle, } from 'react-native';
|
|
6
6
|
import Animated, { Easing, useAnimatedStyle, useSharedValue, withDelay, withTiming, } from 'react-native-reanimated';
|
|
7
7
|
import { StyleSheet } from 'react-native-unistyles';
|
|
8
8
|
import { useTheme } from '../../hooks';
|
|
@@ -120,7 +120,12 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
|
|
|
120
120
|
});
|
|
121
121
|
const footer = (_jsxs(View, { style: styles.footer, children: [onPressPrimaryButton && primaryButtonText ? (_jsx(Button, { onPress: handlePrimaryButtonPress, text: primaryButtonText, inverted: isBrandBackground && inNavModal, ...primaryButtonProps, variant: primaryButtonProps?.variant ?? 'solid', colorScheme: primaryButtonProps?.colorScheme ?? 'highlight' })) : null, onPressSecondaryButton && secondaryButtonText ? (_jsx(Button, { onPress: handleSecondaryButtonPress, text: secondaryButtonText, inverted: isBrandBackground && inNavModal, ...secondaryButtonProps, variant: secondaryButtonProps?.variant ?? 'outline', colorScheme: secondaryButtonProps?.colorScheme ?? 'functional' })) : null] }));
|
|
122
122
|
const InNavModalContainer = scrollable ? ScrollView : View;
|
|
123
|
-
const content = (_jsx(_Fragment, { children: loading ? (_jsxs(View, { style: styles.loadingContainer, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Loading' : undefined, screenReaderFocusable: true, ref: viewRef, children: [_jsx(Spinner, { size: "lg", color: isBrandBackground && inNavModal ? theme.color.icon.inverted : undefined }), _jsx(Heading, { size: "lg", textAlign: "center", inverted: isBrandBackground && inNavModal, children: loadingHeading })] })) : (_jsxs(View, { style: styles.container, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Modal content' : undefined, screenReaderFocusable: true, ref: viewRef, children: [_jsxs(View, { style: styles.header, children: [_jsxs(View, { style: styles.headerTextContent, children: [heading && !image ? (_jsx(Heading, { size: "lg", accessible: true, inverted: isBrandBackground && inNavModal, children: heading })) : null, description && !image ? (_jsx(BodyText, { accessible: true, inverted: isBrandBackground && inNavModal, children: description })) : null] }), showCloseButton ? (_jsx(UnstyledIconButton, { icon: CloseMediumIcon, onPress: handleCloseButtonPress, accessibilityLabel: "Close modal", inverted: isBrandBackground && inNavModal, ...closeButtonProps })) : null] }), image ? (_jsxs(View, { style: styles.imageContainer, children: [image, _jsxs(View, { style: styles.textContent, children: [heading ? (_jsx(Heading, { size: "lg", textAlign: "center", accessible: true, inverted: isBrandBackground && inNavModal, children: heading })) : null, description ? (_jsx(BodyText, { textAlign: "center", accessible: true, inverted: isBrandBackground && inNavModal, children: description })) : null] })] })) : null, inNavModal && (_jsxs(InNavModalContainer, { style: {
|
|
123
|
+
const content = (_jsx(_Fragment, { children: loading ? (_jsxs(View, { style: styles.loadingContainer, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Loading' : undefined, screenReaderFocusable: true, ref: viewRef, children: [_jsx(Spinner, { size: "lg", color: isBrandBackground && inNavModal ? theme.color.icon.inverted : undefined }), _jsx(Heading, { size: "lg", textAlign: "center", inverted: isBrandBackground && inNavModal, children: loadingHeading })] })) : (_jsxs(View, { style: styles.container, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Modal content' : undefined, screenReaderFocusable: true, ref: viewRef, children: [_jsxs(View, { style: styles.header, children: [_jsxs(View, { style: styles.headerTextContent, children: [heading && !image ? (_jsx(Heading, { size: "lg", accessible: true, inverted: isBrandBackground && inNavModal, children: heading })) : null, description && !image ? (_jsx(BodyText, { accessible: true, inverted: isBrandBackground && inNavModal, children: description })) : null] }), showCloseButton ? (_jsx(UnstyledIconButton, { icon: CloseMediumIcon, onPress: handleCloseButtonPress, accessibilityLabel: "Close modal", inverted: isBrandBackground && inNavModal, ...closeButtonProps })) : null] }), image ? (_jsxs(View, { style: styles.imageContainer, children: [image, _jsxs(View, { style: styles.textContent, children: [heading ? (_jsx(Heading, { size: "lg", textAlign: "center", accessible: true, inverted: isBrandBackground && inNavModal, children: heading })) : null, description ? (_jsx(BodyText, { textAlign: "center", accessible: true, inverted: isBrandBackground && inNavModal, children: description })) : null] })] })) : null, inNavModal && (_jsxs(InNavModalContainer, { style: {
|
|
124
|
+
flex: stickyFooter ? 1 : 0,
|
|
125
|
+
...(scrollable ? { marginHorizontal: -1 } : {}),
|
|
126
|
+
}, ...(scrollable ? { contentContainerStyle: { paddingHorizontal: 1 } } : {}), children: [children, !stickyFooter ? (_jsx(View, { style: styles.inNavModalFooterContainer, children: footer })) : null] })), !inNavModal && children, ((!stickyFooter && !inNavModal) || (inNavModal && stickyFooter)) && !noButtons
|
|
127
|
+
? footer
|
|
128
|
+
: null] })) }));
|
|
124
129
|
const renderFooter = useCallback((props) => (_jsx(BottomSheetFooter, { ...props, children: _jsx(View, { style: styles.footerWrap, children: footer }) })), [
|
|
125
130
|
onPressPrimaryButton,
|
|
126
131
|
primaryButtonText,
|
|
@@ -129,7 +134,7 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
|
|
|
129
134
|
primaryButtonProps,
|
|
130
135
|
secondaryButtonProps,
|
|
131
136
|
]);
|
|
132
|
-
return inNavModal ? (_jsxs(View, { onLayout:
|
|
137
|
+
return inNavModal ? (_jsxs(View, { onLayout: e => {
|
|
133
138
|
setInNavModalHeight(e.nativeEvent.layout.height);
|
|
134
139
|
}, style: {
|
|
135
140
|
flex: 1,
|
|
@@ -240,7 +245,7 @@ const styles = StyleSheet.create((theme, rt) => ({
|
|
|
240
245
|
borderTopLeftRadius: theme.components.modal.borderRadius,
|
|
241
246
|
borderTopRightRadius: theme.components.modal.borderRadius,
|
|
242
247
|
backgroundColor: theme.color.surface.neutral.strong,
|
|
243
|
-
paddingBottom: theme.components.
|
|
248
|
+
paddingBottom: theme.components.bottomSheet.padding + rt.insets.bottom,
|
|
244
249
|
variants: {
|
|
245
250
|
background: {
|
|
246
251
|
primary: {},
|
|
@@ -250,22 +255,22 @@ const styles = StyleSheet.create((theme, rt) => ({
|
|
|
250
255
|
},
|
|
251
256
|
fullscreen: {
|
|
252
257
|
true: {
|
|
253
|
-
padding: theme.components.
|
|
258
|
+
padding: theme.components.bottomSheet.padding,
|
|
254
259
|
paddingTop: rt.insets.top,
|
|
255
260
|
},
|
|
256
261
|
false: {
|
|
257
|
-
padding: theme.components.
|
|
258
|
-
}
|
|
259
|
-
}
|
|
262
|
+
padding: theme.components.bottomSheet.padding,
|
|
263
|
+
},
|
|
264
|
+
},
|
|
260
265
|
},
|
|
261
266
|
},
|
|
262
267
|
inNavModalFooterContainer: {
|
|
263
|
-
paddingTop: theme.components.
|
|
268
|
+
paddingTop: theme.components.bottomSheet.padding,
|
|
264
269
|
},
|
|
265
270
|
androidContainer: {
|
|
266
271
|
height: rt.insets.top + 18,
|
|
267
|
-
paddingLeft: theme.components.
|
|
268
|
-
paddingRight: theme.components.
|
|
272
|
+
paddingLeft: theme.components.bottomSheet.padding,
|
|
273
|
+
paddingRight: theme.components.bottomSheet.padding,
|
|
269
274
|
justifyContent: 'flex-end',
|
|
270
275
|
},
|
|
271
276
|
pretendContent: {
|
|
@@ -153,7 +153,7 @@ const SegmentedControl = ({ value: controlledValue, defaultValue, onValueChange,
|
|
|
153
153
|
size,
|
|
154
154
|
registerOptionLayout,
|
|
155
155
|
}), [currentValue, select, disabled, size, registerOptionLayout]);
|
|
156
|
-
return (_jsx(SegmentedControlContext.Provider, { value: contextValue, children: _jsxs(View, { accessibilityRole: "radiogroup", accessibilityState: { disabled }, style: [styles.container, computedStyles, style], ...remainingProps, children: [hasIndicator ? (_jsx(Indicator, {
|
|
156
|
+
return (_jsx(SegmentedControlContext.Provider, { value: contextValue, children: _jsxs(View, { accessibilityRole: "radiogroup", accessibilityState: { disabled }, style: [styles.container, computedStyles, style], ...remainingProps, children: [hasIndicator ? (_jsx(Indicator, { style: [styles.indicator, styles.pointerEventsNone, indicatorStyle] })) : null, children] }) }));
|
|
157
157
|
};
|
|
158
158
|
SegmentedControl.displayName = 'SegmentedControl';
|
|
159
159
|
const styles = StyleSheet.create(theme => ({
|
|
@@ -192,5 +192,8 @@ const styles = StyleSheet.create(theme => ({
|
|
|
192
192
|
borderRadius: theme.components.segmentedControl.borderRadius,
|
|
193
193
|
backgroundColor: theme.color.interactive.brand.surface.strong.default,
|
|
194
194
|
},
|
|
195
|
+
pointerEventsNone: {
|
|
196
|
+
pointerEvents: 'none',
|
|
197
|
+
},
|
|
195
198
|
}));
|
|
196
199
|
export default SegmentedControl;
|
|
@@ -36,7 +36,7 @@ const SegmentedControlOptionRoot = ({ value, children, icon, accessibilityLabel,
|
|
|
36
36
|
const accessibleLabel = typeof children === 'string' || typeof children === 'number' ? String(children) : value;
|
|
37
37
|
return (_jsx(Pressable, { ...props, accessibilityRole: "radio", accessibilityState: { checked: selected, disabled: isDisabled }, accessibilityLabel: accessibilityLabel ?? accessibleLabel, onPress: onPress, onLayout: e => registerOptionLayout(value, e.nativeEvent.layout), disabled: isDisabled, style: [styles.option, style], ...(Platform.OS === 'web'
|
|
38
38
|
? { 'aria-label': accessibilityLabel ?? accessibleLabel }
|
|
39
|
-
: null), children: _jsxs(View, { style: styles.contentWrap, accessible: false, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", ...(Platform.OS === 'web' ? { 'aria-hidden': true } : null), children: [icon ? _jsx(Icon, { as: icon, size: "sm", style: styles.icon }) : null, _jsxs(View, { style: styles.labelWrap, children: [_jsx(BodyText, { size: "md", weight: "semibold", style: styles.labelSizer, accessible: false, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", ...(Platform.OS === 'web' ? { 'aria-hidden': true } : null), children: children }), _jsx(AnimatedView, {
|
|
39
|
+
: null), children: _jsxs(View, { style: styles.contentWrap, accessible: false, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", ...(Platform.OS === 'web' ? { 'aria-hidden': true } : null), children: [icon ? _jsx(Icon, { as: icon, size: "sm", style: styles.icon }) : null, _jsxs(View, { style: styles.labelWrap, children: [_jsx(BodyText, { size: "md", weight: "semibold", style: styles.labelSizer, accessible: false, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", ...(Platform.OS === 'web' ? { 'aria-hidden': true } : null), children: children }), _jsx(AnimatedView, { style: [styles.textLayer, styles.pointerEventsNone, regularLabelStyle], accessible: false, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", ...(Platform.OS === 'web' ? { 'aria-hidden': true } : null), children: _jsx(BodyText, { size: "md", weight: "regular", style: styles.textRegular, children: children }) }), _jsx(AnimatedView, { style: [styles.textLayer, styles.pointerEventsNone, selectedLabelStyle], accessible: false, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", ...(Platform.OS === 'web' ? { 'aria-hidden': true } : null), children: _jsx(BodyText, { size: "md", weight: "semibold", style: styles.textSelected, children: children }) })] })] }) }));
|
|
40
40
|
};
|
|
41
41
|
const SegmentedControlOption = createPressable({ Root: SegmentedControlOptionRoot });
|
|
42
42
|
SegmentedControlOption.displayName = 'SegmentedControlOption';
|
|
@@ -112,6 +112,9 @@ const styles = StyleSheet.create(theme => ({
|
|
|
112
112
|
alignItems: 'center',
|
|
113
113
|
justifyContent: 'center',
|
|
114
114
|
},
|
|
115
|
+
pointerEventsNone: {
|
|
116
|
+
pointerEvents: 'none',
|
|
117
|
+
},
|
|
115
118
|
icon: {
|
|
116
119
|
variants: {
|
|
117
120
|
selected: {
|
|
@@ -26,7 +26,12 @@ const TimePickerWheel = ({ value, setValue = () => { }, items }) => {
|
|
|
26
26
|
setValue(item.value);
|
|
27
27
|
}
|
|
28
28
|
}, [setValue, value]);
|
|
29
|
-
const renderOverlay = useCallback(() => (_jsxs(View, { style: [styles.overlayContainer
|
|
29
|
+
const renderOverlay = useCallback(() => (_jsxs(View, { style: [styles.overlayContainer, styles.pointerEventsNone], children: [_jsx(View, { style: [styles.fadeOverlay, styles.pointerEventsNone, { height: fadeHeight }], children: _jsxs(Svg, { width: "100%", height: "100%", preserveAspectRatio: "none", children: [_jsx(Defs, { children: _jsxs(LinearGradient, { id: `${gradientId}-top`, x1: "0", y1: "0", x2: "0", y2: "1", children: [_jsx(Stop, { offset: "0", stopColor: theme.color.background.secondary, stopOpacity: 1 }), _jsx(Stop, { offset: "1", stopColor: theme.color.background.secondary, stopOpacity: 0 })] }) }), _jsx(Rect, { width: "100%", height: "100%", fill: `url(#${gradientId}-top)` })] }) }), _jsx(View, { style: [
|
|
30
|
+
styles.fadeOverlay,
|
|
31
|
+
styles.fadeOverlayBottom,
|
|
32
|
+
styles.pointerEventsNone,
|
|
33
|
+
{ height: fadeHeight },
|
|
34
|
+
], children: _jsxs(Svg, { width: "100%", height: "100%", preserveAspectRatio: "none", children: [_jsx(Defs, { children: _jsxs(LinearGradient, { id: `${gradientId}-bottom`, x1: "0", y1: "0", x2: "0", y2: "1", children: [_jsx(Stop, { offset: "0", stopColor: theme.color.background.secondary, stopOpacity: 0 }), _jsx(Stop, { offset: "1", stopColor: theme.color.background.secondary, stopOpacity: 1 })] }) }), _jsx(Rect, { width: "100%", height: "100%", fill: `url(#${gradientId}-bottom)` })] }) })] })), [fadeHeight, gradientId, theme.color.background.secondary]);
|
|
30
35
|
const renderItem = useCallback(({ item }) => (_jsx(View, { style: styles.indicator, children: _jsx(BodyText, { size: "lg", children: item.label }) })), []);
|
|
31
36
|
return (_jsxs(View, { style: [styles.container, { height: pickerHeight }], children: [_jsx(View, { style: styles.overlayContainer, children: _jsx(View, { style: [styles.selection] }) }), _jsx(WheelPicker, { data: data, value: value, onValueChanged: handleValueChanged, itemHeight: ITEM_HEIGHT, visibleItemCount: displayCount, width: theme.components.timePicker.time.item.width, renderItem: renderItem, renderOverlay: renderOverlay })] }));
|
|
32
37
|
};
|
|
@@ -74,5 +79,8 @@ const styles = StyleSheet.create(theme => ({
|
|
|
74
79
|
top: undefined,
|
|
75
80
|
bottom: 0,
|
|
76
81
|
},
|
|
82
|
+
pointerEventsNone: {
|
|
83
|
+
pointerEvents: 'none',
|
|
84
|
+
},
|
|
77
85
|
}));
|
|
78
86
|
export default TimePickerWheel;
|
|
@@ -63,7 +63,7 @@ export const ToastProvider = ({ children, safeAreaPadding = true, }) => {
|
|
|
63
63
|
timers.current = {};
|
|
64
64
|
};
|
|
65
65
|
}, []);
|
|
66
|
-
return (_jsxs(ToastContext.Provider, { value: { addToast, removeToast }, children: [children, _jsx(View, {
|
|
66
|
+
return (_jsxs(ToastContext.Provider, { value: { addToast, removeToast }, children: [children, _jsx(View, { style: styles.container, children: _jsx(View, { style: styles.stack, children: toasts.map(t => (_jsx(ToastItem, { ref: el => {
|
|
67
67
|
toastRefs.current[t.id] = el;
|
|
68
68
|
}, toast: t, onClose: removeToast }, t.id))) }) })] }));
|
|
69
69
|
};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
3
|
-
import {
|
|
2
|
+
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
3
|
+
import { TextInput, View } from 'react-native';
|
|
4
4
|
import { StyleSheet } from 'react-native-unistyles';
|
|
5
5
|
import { FormField } from '../FormField';
|
|
6
|
+
import { getNextIndexFromValueChange } from './VerificationInput.utils';
|
|
6
7
|
import { VerificationInputSlot } from './VerificationInputSlot';
|
|
7
|
-
const VerificationInput = forwardRef(({ value = '', onChangeText, label, labelVariant = 'body', helperText, helperIcon, validationStatus = 'initial', validText, invalidText, disabled = false, readonly = false, secureTextEntry = false, autoFocus = false, style, ...props }, ref) => {
|
|
8
|
+
const VerificationInput = forwardRef(({ value = '', onChangeText, label, labelVariant = 'body', helperText, helperIcon, validationStatus = 'initial', validText, invalidText, disabled = false, readonly = false, secureTextEntry = false, autoFocus = false, style, testID, ...props }, ref) => {
|
|
8
9
|
const length = 6;
|
|
9
10
|
const inputRef = useRef(null);
|
|
10
11
|
const latestValueRef = useRef(value);
|
|
@@ -26,12 +27,12 @@ const VerificationInput = forwardRef(({ value = '', onChangeText, label, labelVa
|
|
|
26
27
|
setSelection(nextSelection);
|
|
27
28
|
}
|
|
28
29
|
}, [length, value]);
|
|
29
|
-
const updateValue = (nextValue) => {
|
|
30
|
+
const updateValue = useCallback((nextValue) => {
|
|
30
31
|
const trimmedValue = nextValue.slice(0, length);
|
|
31
32
|
latestValueRef.current = trimmedValue;
|
|
32
33
|
setDisplayValue(trimmedValue);
|
|
33
34
|
onChangeText?.(trimmedValue);
|
|
34
|
-
};
|
|
35
|
+
}, [length, onChangeText]);
|
|
35
36
|
const setSelectionIndex = (index) => {
|
|
36
37
|
const clampedIndex = Math.max(0, Math.min(index, length));
|
|
37
38
|
const hasChar = !!latestValueRef.current[clampedIndex];
|
|
@@ -50,15 +51,6 @@ const VerificationInput = forwardRef(({ value = '', onChangeText, label, labelVa
|
|
|
50
51
|
setSelection(nextSelection);
|
|
51
52
|
setFocusedIndex(Math.min(clampedIndex, length - 1));
|
|
52
53
|
};
|
|
53
|
-
const findDiffIndex = (prevValue, nextValue) => {
|
|
54
|
-
const minLength = Math.min(prevValue.length, nextValue.length);
|
|
55
|
-
for (let i = 0; i < minLength; i += 1) {
|
|
56
|
-
if (prevValue[i] !== nextValue[i]) {
|
|
57
|
-
return i;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return minLength;
|
|
61
|
-
};
|
|
62
54
|
const handleChangeText = (text) => {
|
|
63
55
|
const prevValue = latestValueRef.current;
|
|
64
56
|
const nextValue = text.slice(0, length);
|
|
@@ -67,11 +59,7 @@ const VerificationInput = forwardRef(({ value = '', onChangeText, label, labelVa
|
|
|
67
59
|
const diff = nextLength - prevLength;
|
|
68
60
|
const isBulkInsert = text.length > 1 && diff > 1;
|
|
69
61
|
const shouldBlur = nextLength >= length;
|
|
70
|
-
|
|
71
|
-
if (Platform.OS === 'android') {
|
|
72
|
-
const editedIndex = findDiffIndex(prevValue, nextValue);
|
|
73
|
-
nextIndex = diff >= 0 ? Math.min(editedIndex + 1, length) : Math.max(editedIndex, 0);
|
|
74
|
-
}
|
|
62
|
+
const nextIndex = getNextIndexFromValueChange({ prevValue, nextValue, length });
|
|
75
63
|
updateValue(nextValue);
|
|
76
64
|
if (isBulkInsert) {
|
|
77
65
|
setCaretIndex(Math.min(nextLength, length));
|
|
@@ -135,7 +123,7 @@ const VerificationInput = forwardRef(({ value = '', onChangeText, label, labelVa
|
|
|
135
123
|
setSelectionIndex(index);
|
|
136
124
|
}
|
|
137
125
|
},
|
|
138
|
-
}), [length,
|
|
126
|
+
}), [length, updateValue]);
|
|
139
127
|
const slots = Array.from({ length }, (_, index) => index);
|
|
140
128
|
const getAccessibilityLabel = () => {
|
|
141
129
|
return label || props.accessibilityLabel;
|
|
@@ -172,7 +160,7 @@ const VerificationInput = forwardRef(({ value = '', onChangeText, label, labelVa
|
|
|
172
160
|
latestSelectionRef.current = nextSelection;
|
|
173
161
|
setSelection(nextSelection);
|
|
174
162
|
setFocusedIndex(Math.min(nextSelection.start, length - 1));
|
|
175
|
-
}, onFocus: handleFocus, onBlur: handleBlur, selection: selection, keyboardType: "number-pad", textContentType: "oneTimeCode", autoComplete: "sms-otp", secureTextEntry: secureTextEntry, maxLength: length, caretHidden: true, style: styles.hiddenInput,
|
|
163
|
+
}, onFocus: handleFocus, onBlur: handleBlur, selection: selection, keyboardType: "number-pad", textContentType: "oneTimeCode", autoComplete: "sms-otp", secureTextEntry: secureTextEntry, maxLength: length, caretHidden: true, style: styles.hiddenInput, testID: testID }), slots.map(index => {
|
|
176
164
|
const char = displayValue[index] || '';
|
|
177
165
|
const isActive = focusedIndex === index;
|
|
178
166
|
const displayChar = secureTextEntry && char ? '*' : char;
|
|
@@ -199,11 +187,12 @@ const styles = StyleSheet.create(theme => ({
|
|
|
199
187
|
position: 'absolute',
|
|
200
188
|
width: '100%',
|
|
201
189
|
height: '100%',
|
|
190
|
+
pointerEvents: 'none',
|
|
202
191
|
left: 0,
|
|
203
192
|
top: 0,
|
|
204
193
|
color: 'transparent',
|
|
205
194
|
fontSize: 1,
|
|
206
|
-
opacity: 0.
|
|
195
|
+
opacity: 0.01,
|
|
207
196
|
},
|
|
208
197
|
}));
|
|
209
198
|
VerificationInput.displayName = 'VerificationInput';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
interface GetNextIndexFromValueChangeOptions {
|
|
2
|
+
prevValue: string;
|
|
3
|
+
nextValue: string;
|
|
4
|
+
length: number;
|
|
5
|
+
}
|
|
6
|
+
export declare const findDiffIndex: (prevValue: string, nextValue: string) => number;
|
|
7
|
+
export declare const getNextIndexFromValueChange: ({ prevValue, nextValue, length, }: GetNextIndexFromValueChangeOptions) => number;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const findDiffIndex = (prevValue, nextValue) => {
|
|
2
|
+
const minLength = Math.min(prevValue.length, nextValue.length);
|
|
3
|
+
for (let i = 0; i < minLength; i += 1) {
|
|
4
|
+
if (prevValue[i] !== nextValue[i]) {
|
|
5
|
+
return i;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
return minLength;
|
|
9
|
+
};
|
|
10
|
+
export const getNextIndexFromValueChange = ({ prevValue, nextValue, length, }) => {
|
|
11
|
+
const diff = nextValue.length - prevValue.length;
|
|
12
|
+
const editedIndex = findDiffIndex(prevValue, nextValue);
|
|
13
|
+
if (diff >= 0) {
|
|
14
|
+
return Math.min(editedIndex + 1, length);
|
|
15
|
+
}
|
|
16
|
+
return Math.max(editedIndex, 0);
|
|
17
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { findDiffIndex, getNextIndexFromValueChange } from './VerificationInput.utils';
|
|
3
|
+
describe('findDiffIndex', () => {
|
|
4
|
+
it('returns first differing index', () => {
|
|
5
|
+
expect(findDiffIndex('12', '19')).toBe(1);
|
|
6
|
+
});
|
|
7
|
+
it('returns previous length when next appends', () => {
|
|
8
|
+
expect(findDiffIndex('12', '123')).toBe(2);
|
|
9
|
+
});
|
|
10
|
+
it('returns next length when next shortens with same prefix', () => {
|
|
11
|
+
expect(findDiffIndex('123', '12')).toBe(2);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
describe('getNextIndexFromValueChange', () => {
|
|
15
|
+
it('moves to the slot after the one that changed when inserting in an empty later slot', () => {
|
|
16
|
+
expect(getNextIndexFromValueChange({
|
|
17
|
+
prevValue: '12',
|
|
18
|
+
nextValue: '123',
|
|
19
|
+
length: 6,
|
|
20
|
+
})).toBe(3);
|
|
21
|
+
});
|
|
22
|
+
it('caps at length when value becomes full', () => {
|
|
23
|
+
expect(getNextIndexFromValueChange({
|
|
24
|
+
prevValue: '12345',
|
|
25
|
+
nextValue: '123456',
|
|
26
|
+
length: 6,
|
|
27
|
+
})).toBe(6);
|
|
28
|
+
});
|
|
29
|
+
it('stays at edited index for deletions', () => {
|
|
30
|
+
expect(getNextIndexFromValueChange({
|
|
31
|
+
prevValue: '123',
|
|
32
|
+
nextValue: '12',
|
|
33
|
+
length: 6,
|
|
34
|
+
})).toBe(2);
|
|
35
|
+
});
|
|
36
|
+
});
|
package/docs/changelog.mdx
CHANGED
|
@@ -9,6 +9,119 @@ import { BackToTopButton } from './components';
|
|
|
9
9
|
The changelog for the Hearth React Native library. Here you can find all the changes, improvements, and bug fixes for each version.
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
## 0.27.1
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- [#990](https://github.com/utilitywarehouse/hearth/pull/990) [`958e0e1`](https://github.com/utilitywarehouse/hearth/commit/958e0e1a9d5451d1e11fecadc69ae3c5ad9d42ca) Thanks [@declanelcocks](https://github.com/declanelcocks)! - 🐛 [FIX]: Fix `Modal` layout when `inNavModal` and `stickyFooter={false}`.
|
|
17
|
+
|
|
18
|
+
Corrects the container flex style for `inNavModal` modals with a non-sticky footer, where the UX was not great when scrolling.
|
|
19
|
+
|
|
20
|
+
**Components affected**:
|
|
21
|
+
- `Modal`
|
|
22
|
+
|
|
23
|
+
**Developer changes**:
|
|
24
|
+
|
|
25
|
+
No changes required.
|
|
26
|
+
|
|
27
|
+
- [#992](https://github.com/utilitywarehouse/hearth/pull/992) [`2560b3d`](https://github.com/utilitywarehouse/hearth/commit/2560b3dcba7ed4981fad585628f96afd07d8de4f) Thanks [@jordmccord](https://github.com/jordmccord)! - 💅 [ENHANCEMENT]: Add optional leading `icon` support to `SegmentedControlOption`.
|
|
28
|
+
|
|
29
|
+
This adds an optional `icon` prop to `SegmentedControlOption`, allowing icons to be displayed before option labels in segmented controls.
|
|
30
|
+
|
|
31
|
+
Docs and stories were updated to include icon usage examples.
|
|
32
|
+
|
|
33
|
+
**Components affected**:
|
|
34
|
+
- `SegmentedControlOption`
|
|
35
|
+
|
|
36
|
+
**Developer changes**:
|
|
37
|
+
|
|
38
|
+
No changes required for existing usage.
|
|
39
|
+
|
|
40
|
+
To use the new optional icon prop:
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import { SegmentedControl, SegmentedControlOption } from '@utilitywarehouse/hearth-react-native';
|
|
44
|
+
import { ElectricitySmallIcon } from '@utilitywarehouse/hearth-react-native-icons';
|
|
45
|
+
|
|
46
|
+
<SegmentedControl defaultValue="energy">
|
|
47
|
+
<SegmentedControlOption value="energy" icon={ElectricitySmallIcon}>
|
|
48
|
+
Energy
|
|
49
|
+
</SegmentedControlOption>
|
|
50
|
+
<SegmentedControlOption value="broadband">Broadband</SegmentedControlOption>
|
|
51
|
+
</SegmentedControl>;
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 0.27.0
|
|
55
|
+
|
|
56
|
+
### Minor Changes
|
|
57
|
+
|
|
58
|
+
- [#987](https://github.com/utilitywarehouse/hearth/pull/987) [`eb962d2`](https://github.com/utilitywarehouse/hearth/commit/eb962d2f33b63fa3aeda0b291fd41ace90d04c41) Thanks [@jordmccord](https://github.com/jordmccord)! - 🌟 [FEATURE]: Add `SegmentedControl` and `SegmentedControlOption` components.
|
|
59
|
+
|
|
60
|
+
This introduces a new segmented control component for switching between a small set of related options.
|
|
61
|
+
The component includes controlled and uncontrolled usage, size variants (`sm`, `md`), animated selected indicator movement, and improved accessibility semantics for screen readers.
|
|
62
|
+
|
|
63
|
+
**Components affected**:
|
|
64
|
+
- `SegmentedControl`
|
|
65
|
+
- `SegmentedControlOption`
|
|
66
|
+
|
|
67
|
+
**Developer changes**:
|
|
68
|
+
|
|
69
|
+
Import and compose the new components as follows:
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
import { SegmentedControl, SegmentedControlOption } from '@utilitywarehouse/hearth-react-native';
|
|
73
|
+
|
|
74
|
+
<SegmentedControl defaultValue="day" size="sm">
|
|
75
|
+
<SegmentedControlOption value="day">Day</SegmentedControlOption>
|
|
76
|
+
<SegmentedControlOption value="week">Week</SegmentedControlOption>
|
|
77
|
+
<SegmentedControlOption value="month">Month</SegmentedControlOption>
|
|
78
|
+
</SegmentedControl>;
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Patch Changes
|
|
82
|
+
|
|
83
|
+
- [#989](https://github.com/utilitywarehouse/hearth/pull/989) [`c97122e`](https://github.com/utilitywarehouse/hearth/commit/c97122eb429ec4adef656fb245a9256a5619df61) Thanks [@jordmccord](https://github.com/jordmccord)! - 🐛 [FIX]: Ensure horizontal `Banner` fills available width when `onPress` is not provided.
|
|
84
|
+
|
|
85
|
+
Fixed a layout issue where a horizontal `Banner` without `onPress` could fail to stretch correctly within its parent container.
|
|
86
|
+
|
|
87
|
+
**Components affected**:
|
|
88
|
+
- `Banner`
|
|
89
|
+
|
|
90
|
+
**Developer changes**:
|
|
91
|
+
|
|
92
|
+
No changes required.
|
|
93
|
+
|
|
94
|
+
## 0.26.0
|
|
95
|
+
|
|
96
|
+
### Minor Changes
|
|
97
|
+
|
|
98
|
+
- [#981](https://github.com/utilitywarehouse/hearth/pull/981) [`df56387`](https://github.com/utilitywarehouse/hearth/commit/df563872e6bf040d419f6c7fce2343ebe560edb9) Thanks [@declanelcocks](https://github.com/declanelcocks)! - 🌟 [ENHANCEMENT]: Improve `Modal` behaviour when used inside a React Navigation modal (`inNavModal`).
|
|
99
|
+
|
|
100
|
+
The following improvements have been made to the `Modal` component when used in a navigation context with `inNavModal={true}`:
|
|
101
|
+
- **`scrollable` prop**: Content is now rendered inside a `ScrollView` by default. Set `scrollable={false}` to opt out, for example when you need to centre content or use a custom layout.
|
|
102
|
+
- **`stickyFooter` support**: The `stickyFooter` prop now works correctly in `inNavModal` mode.
|
|
103
|
+
- **Auto full-screen detection**: When the modal fills the entire screen (e.g. with `presentation: 'fullScreenModal'`), the `fullscreen` style is applied automatically. The `fullscreen` prop is no longer available when `inNavModal` is `true`.
|
|
104
|
+
|
|
105
|
+
**Components affected**:
|
|
106
|
+
- `Modal`
|
|
107
|
+
|
|
108
|
+
**Developer changes**:
|
|
109
|
+
|
|
110
|
+
No changes are required for existing usage. If you were passing `fullscreen` alongside `inNavModal={true}`, remove the `fullscreen` prop — full-screen styling is now detected automatically:
|
|
111
|
+
|
|
112
|
+
```diff
|
|
113
|
+
- <Modal inNavModal fullscreen>
|
|
114
|
+
+ <Modal inNavModal>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
To disable the default `ScrollView` wrapping in `inNavModal` mode:
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
<Modal inNavModal scrollable={false}>
|
|
121
|
+
{/* custom layout */}
|
|
122
|
+
</Modal>
|
|
123
|
+
```
|
|
124
|
+
|
|
12
125
|
## 0.25.0
|
|
13
126
|
|
|
14
127
|
### Minor Changes
|