@utilitywarehouse/hearth-react-native 0.28.0 → 0.28.1-testid-fix-2

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.
Files changed (60) hide show
  1. package/.turbo/turbo-build.log +5 -4
  2. package/.turbo/turbo-lint.log +70 -68
  3. package/CHANGELOG.md +6 -0
  4. package/build/components/DatePicker/TimePicker.d.ts +3 -0
  5. package/build/components/DatePicker/TimePicker.js +84 -0
  6. package/build/components/DatePicker/time-picker/animated-math.d.ts +4 -0
  7. package/build/components/DatePicker/time-picker/animated-math.js +19 -0
  8. package/build/components/DatePicker/time-picker/period-native.d.ts +6 -0
  9. package/build/components/DatePicker/time-picker/period-native.js +17 -0
  10. package/build/components/DatePicker/time-picker/period-picker.d.ts +6 -0
  11. package/build/components/DatePicker/time-picker/period-picker.js +10 -0
  12. package/build/components/DatePicker/time-picker/period-web.d.ts +6 -0
  13. package/build/components/DatePicker/time-picker/period-web.js +21 -0
  14. package/build/components/DatePicker/time-picker/wheel-native.d.ts +8 -0
  15. package/build/components/DatePicker/time-picker/wheel-native.js +19 -0
  16. package/build/components/DatePicker/time-picker/wheel-picker/index.d.ts +2 -0
  17. package/build/components/DatePicker/time-picker/wheel-picker/index.js +2 -0
  18. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.d.ts +16 -0
  19. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.js +97 -0
  20. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.d.ts +21 -0
  21. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.js +88 -0
  22. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.d.ts +23 -0
  23. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.js +21 -0
  24. package/build/components/DatePicker/time-picker/wheel-web.d.ts +8 -0
  25. package/build/components/DatePicker/time-picker/wheel-web.js +146 -0
  26. package/build/components/DatePicker/time-picker/wheel.d.ts +8 -0
  27. package/build/components/DatePicker/time-picker/wheel.js +10 -0
  28. package/build/components/Select/Select.d.ts +1 -1
  29. package/build/components/Select/Select.js +4 -4
  30. package/build/components/Select/Select.props.d.ts +4 -0
  31. package/build/components/Select/SelectOption.d.ts +1 -1
  32. package/build/components/Select/SelectOption.js +2 -2
  33. package/build/components/VerificationInput/VerificationInput.js +3 -3
  34. package/docs/all-components.mdx +2 -2
  35. package/docs/changelog.mdx +178 -1
  36. package/docs/components/AllComponents.web.tsx +65 -125
  37. package/docs/components/NextPrevPage.tsx +28 -44
  38. package/docs/components/index.ts +1 -0
  39. package/docs/getting-started.mdx +3 -3
  40. package/docs/hooks.mdx +2 -2
  41. package/docs/introduction.mdx +1 -1
  42. package/docs/layout-components.docs.mdx +11 -2
  43. package/docs/styling.mdx +3 -3
  44. package/package.json +3 -3
  45. package/scripts/copyChangelog.js +8 -1
  46. package/src/components/Banner/Banner.docs.mdx +2 -1
  47. package/src/components/Center/Center.docs.mdx +6 -5
  48. package/src/components/Checkbox/Checkbox.docs.mdx +2 -1
  49. package/src/components/CurrencyInput/CurrencyInput.docs.mdx +2 -1
  50. package/src/components/HighlightBanner/HighlightBanner.docs.mdx +2 -1
  51. package/src/components/Input/Input.docs.mdx +2 -1
  52. package/src/components/Menu/Menu.docs.mdx +2 -1
  53. package/src/components/Modal/Modal.docs.mdx +2 -1
  54. package/src/components/Radio/Radio.docs.mdx +2 -1
  55. package/src/components/Select/Select.docs.mdx +2 -1
  56. package/src/components/Select/Select.props.ts +4 -0
  57. package/src/components/Select/Select.tsx +9 -2
  58. package/src/components/Select/SelectOption.tsx +2 -0
  59. package/src/components/Switch/Switch.docs.mdx +3 -2
  60. package/src/components/VerificationInput/VerificationInput.tsx +3 -0
@@ -0,0 +1,88 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { memo, useEffect, useMemo, useRef, useState } from 'react';
3
+ import { Animated, Platform, View, } from 'react-native';
4
+ import WheelPickerItem from './wheel-picker-item';
5
+ import styles from './wheel-picker.style';
6
+ const WheelPicker = ({ value, options, onChange, selectedIndicatorStyle = {}, containerStyle = {}, itemStyle = {}, itemHeight = 40, scaleFunction = (x) => 1.0 ** x, rotationFunction = (x) => 1 - Math.pow(1 / 2, x), opacityFunction = (x) => Math.pow(1 / 3, x), visibleRest = 2, decelerationRate = 'normal', containerProps = {}, flatListProps = {}, }) => {
7
+ const momentumStarted = useRef(false);
8
+ const selectedIndex = options.findIndex(item => item.value === value);
9
+ const flatListRef = useRef(null);
10
+ const [scrollY] = useState(new Animated.Value(selectedIndex * itemHeight));
11
+ const containerHeight = (1 + visibleRest * 2) * itemHeight;
12
+ const paddedOptions = useMemo(() => {
13
+ const array = [...options];
14
+ for (let i = 0; i < visibleRest; i++) {
15
+ array.unshift(null);
16
+ array.push(null);
17
+ }
18
+ return array;
19
+ }, [options, visibleRest]);
20
+ const offsets = useMemo(() => [...Array(paddedOptions.length)].map((_, i) => i * itemHeight), [paddedOptions, itemHeight]);
21
+ const currentScrollIndex = useMemo(() => Animated.add(Animated.divide(scrollY, itemHeight), visibleRest), [visibleRest, scrollY, itemHeight]);
22
+ const handleScrollEnd = (event) => {
23
+ const offsetY = Math.min(itemHeight * (options.length - 1), Math.max(event.nativeEvent.contentOffset.y, 0));
24
+ let index = Math.floor(offsetY / itemHeight);
25
+ const remainder = offsetY % itemHeight;
26
+ if (remainder > itemHeight / 2) {
27
+ index++;
28
+ }
29
+ if (index !== selectedIndex) {
30
+ onChange(options[index]?.value || 0);
31
+ }
32
+ };
33
+ const handleMomentumScrollBegin = () => {
34
+ momentumStarted.current = true;
35
+ };
36
+ const handleMomentumScrollEnd = (event) => {
37
+ momentumStarted.current = false;
38
+ handleScrollEnd(event);
39
+ };
40
+ const handleScrollEndDrag = (event) => {
41
+ // Capture the offset value immediately
42
+ const offsetY = event.nativeEvent.contentOffset?.y;
43
+ // We'll start a short timer to see if momentum scroll begins
44
+ setTimeout(() => {
45
+ // If momentum scroll hasn't started within the timeout,
46
+ // then it was a slow scroll that won't trigger momentum
47
+ if (!momentumStarted.current && offsetY !== undefined) {
48
+ // Create a synthetic event with just the data we need
49
+ const syntheticEvent = {
50
+ nativeEvent: {
51
+ contentOffset: { y: offsetY },
52
+ },
53
+ };
54
+ handleScrollEnd(syntheticEvent);
55
+ }
56
+ }, 50);
57
+ };
58
+ useEffect(() => {
59
+ if (selectedIndex < 0 || selectedIndex >= options.length) {
60
+ throw new Error(`Selected index ${selectedIndex} is out of bounds [0, ${options.length - 1}]`);
61
+ }
62
+ }, [selectedIndex, options]);
63
+ /**
64
+ * If selectedIndex is changed from outside (not via onChange) we need to scroll to the specified index.
65
+ * This ensures that what the user sees as selected in the picker always corresponds to the value state.
66
+ */
67
+ useEffect(() => {
68
+ flatListRef.current?.scrollToIndex({
69
+ index: selectedIndex,
70
+ animated: Platform.OS === 'ios',
71
+ });
72
+ }, [selectedIndex, itemHeight]);
73
+ return (_jsxs(View, { style: [styles.container, { height: containerHeight }, containerStyle], ...containerProps, children: [_jsx(View, { style: [
74
+ styles.selectedIndicator,
75
+ selectedIndicatorStyle,
76
+ {
77
+ transform: [{ translateY: -itemHeight / 2 }],
78
+ height: itemHeight,
79
+ },
80
+ ] }), _jsx(Animated.FlatList, { ...flatListProps, ref: flatListRef, nestedScrollEnabled: true, style: styles.scrollView, showsVerticalScrollIndicator: false, onScroll: Animated.event([{ nativeEvent: { contentOffset: { y: scrollY } } }], {
81
+ useNativeDriver: true,
82
+ }), onScrollEndDrag: handleScrollEndDrag, onMomentumScrollBegin: handleMomentumScrollBegin, onMomentumScrollEnd: handleMomentumScrollEnd, snapToOffsets: offsets, decelerationRate: decelerationRate, initialScrollIndex: selectedIndex, getItemLayout: (_, index) => ({
83
+ length: itemHeight,
84
+ offset: itemHeight * index,
85
+ index,
86
+ }), data: paddedOptions, keyExtractor: (item, index) => item ? `${item.value}-${item.text}-${index}` : `null-${index}`, renderItem: ({ item: option, index }) => (_jsx(WheelPickerItem, { index: index, option: option, style: itemStyle, height: itemHeight, currentScrollIndex: currentScrollIndex, scaleFunction: scaleFunction, rotationFunction: rotationFunction, opacityFunction: opacityFunction, visibleRest: visibleRest }, `option-${index}`)) })] }));
87
+ };
88
+ export default memo(WheelPicker);
@@ -0,0 +1,23 @@
1
+ declare const _default: {
2
+ container: {
3
+ position: "relative";
4
+ };
5
+ selectedIndicator: {
6
+ position: "absolute";
7
+ width: "100%";
8
+ top: "50%";
9
+ };
10
+ scrollView: {
11
+ overflow: "hidden";
12
+ flex: number;
13
+ };
14
+ option: {
15
+ alignItems: "center";
16
+ justifyContent: "center";
17
+ paddingHorizontal: number;
18
+ zIndex: number;
19
+ };
20
+ } & {
21
+ useVariants: (variants: never) => void;
22
+ };
23
+ export default _default;
@@ -0,0 +1,21 @@
1
+ import { StyleSheet } from 'react-native-unistyles';
2
+ export default StyleSheet.create({
3
+ container: {
4
+ position: 'relative',
5
+ },
6
+ selectedIndicator: {
7
+ position: 'absolute',
8
+ width: '100%',
9
+ top: '50%',
10
+ },
11
+ scrollView: {
12
+ overflow: 'hidden',
13
+ flex: 1,
14
+ },
15
+ option: {
16
+ alignItems: 'center',
17
+ justifyContent: 'center',
18
+ paddingHorizontal: 16,
19
+ zIndex: 100,
20
+ },
21
+ });
@@ -0,0 +1,8 @@
1
+ import { PickerOption } from '../DatePicker.props';
2
+ interface WheelProps {
3
+ value: number | string;
4
+ setValue?: (value: any) => void;
5
+ items: PickerOption[];
6
+ }
7
+ declare const _default: import("react").MemoExoticComponent<({ value, setValue, items }: WheelProps) => import("react/jsx-runtime").JSX.Element>;
8
+ export default _default;
@@ -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);
@@ -1,6 +1,6 @@
1
1
  import SelectProps from './Select.props';
2
2
  declare const Select: {
3
- ({ options, value, onValueChange, label, labelVariant, placeholder, disabled, leadingIcon: LeadingIcon, validationStatus, helperText, helperIcon, invalidText, validText, required, children, bottomSheetProps, menuHeading, readonly, emptyText, listProps, searchable, searchPlaceholder, ...rest }: SelectProps): import("react/jsx-runtime").JSX.Element;
3
+ ({ options, value, onValueChange, label, labelVariant, placeholder, disabled, leadingIcon: LeadingIcon, validationStatus, helperText, helperIcon, invalidText, validText, required, children, bottomSheetProps, menuHeading, readonly, emptyText, listProps, searchable, searchPlaceholder, testID, ...rest }: SelectProps): import("react/jsx-runtime").JSX.Element;
4
4
  displayName: string;
5
5
  };
6
6
  export default Select;
@@ -12,7 +12,7 @@ import { Input } from '../Input';
12
12
  import { SelectContext } from './Select.context';
13
13
  import SelectOption from './SelectOption';
14
14
  import { SafeAreaView } from '../SafeAreaView';
15
- const Select = ({ options = [], value, onValueChange, label, labelVariant = 'body', placeholder = 'Select an option', disabled = false, leadingIcon: LeadingIcon, validationStatus = 'initial', helperText, helperIcon, invalidText, validText, required = true, children, bottomSheetProps, menuHeading, readonly = false, emptyText = 'No options available', listProps, searchable = false, searchPlaceholder = 'Search', ...rest }) => {
15
+ const Select = ({ options = [], value, onValueChange, label, labelVariant = 'body', placeholder = 'Select an option', disabled = false, leadingIcon: LeadingIcon, validationStatus = 'initial', helperText, helperIcon, invalidText, validText, required = true, children, bottomSheetProps, menuHeading, readonly = false, emptyText = 'No options available', listProps, searchable = false, searchPlaceholder = 'Search', testID, ...rest }) => {
16
16
  const formFieldContext = useFormFieldContext();
17
17
  const validationStatusFromContext = formFieldContext?.validationStatus ?? validationStatus;
18
18
  const isRequired = formFieldContext?.required ?? required;
@@ -65,9 +65,9 @@ const Select = ({ options = [], value, onValueChange, label, labelVariant = 'bod
65
65
  setIsOpen(false);
66
66
  setSearch('');
67
67
  }, []);
68
- const renderSelectOption = useCallback(({ item }) => (_jsx(SelectOption, { label: item.label, value: item.value, disabled: item.disabled, leadingIcon: item.leadingIcon, trailingIcon: item.trailingIcon })), []);
68
+ const renderSelectOption = useCallback(({ item }) => (_jsx(SelectOption, { label: item.label, value: item.value, disabled: item.disabled, leadingIcon: item.leadingIcon, trailingIcon: item.trailingIcon, testID: testID ? `${testID}-option-${item.label}` : undefined })), [testID]);
69
69
  const renderEmptyComponent = useCallback(() => (_jsx(BottomSheetView, { style: styles.emptyContainer, children: _jsx(DetailText, { children: emptyText }) })), [emptyText]);
70
- return (_jsxs(View, { ...rest, style: [styles.container, rest.style], children: [_jsx(FormField, { label: label, labelVariant: labelVariant, helperText: helperText, helperIcon: helperIcon, validationStatus: validationStatusFromContext, required: isRequired, disabled: isDisabled, readonly: isReadonly, invalidText: invalidText, validText: validText, children: _jsxs(Pressable, { onPress: openBottomSheet, disabled: isDisabled || isReadonly, style: ({ pressed }) => [
70
+ return (_jsxs(View, { ...rest, style: [styles.container, rest.style], children: [_jsx(FormField, { label: label, labelVariant: labelVariant, helperText: helperText, helperIcon: helperIcon, validationStatus: validationStatusFromContext, required: isRequired, disabled: isDisabled, readonly: isReadonly, invalidText: invalidText, validText: validText, children: _jsxs(Pressable, { onPress: openBottomSheet, disabled: isDisabled || isReadonly, testID: testID, style: ({ pressed }) => [
71
71
  styles.selectContainer,
72
72
  styles.pressedContainer(pressed || isOpen),
73
73
  ], children: [!!LeadingIcon && (_jsx(View, { children: (() => {
@@ -77,7 +77,7 @@ const Select = ({ options = [], value, onValueChange, label, labelVariant = 'bod
77
77
  selectedValue: value,
78
78
  onValueChange,
79
79
  close: closeBottomSheet,
80
- }, children: _jsxs(SafeAreaView, { edges: ['top'], style: { flex: 1 }, children: [menuHeading && (_jsx(View, { style: styles.headingContainer, children: _jsx(DetailText, { size: "lg", children: menuHeading }) })), searchable && (_jsx(View, { style: styles.searchContainer, children: _jsx(Input, { placeholder: searchPlaceholder, value: search, inBottomSheet: true, onChangeText: setSearch, type: "search" }) })), children ? (_jsx(BottomSheetScrollView, { children: children })) : (_jsx(BottomSheetFlatList, { data: filteredOptions, keyExtractor: (option) => option.value, renderItem: renderSelectOption, ListEmptyComponent: renderEmptyComponent, ...listProps }))] }) }) })] }));
80
+ }, children: _jsxs(SafeAreaView, { edges: ['top'], style: { flex: 1 }, children: [menuHeading && (_jsx(View, { style: styles.headingContainer, children: _jsx(DetailText, { size: "lg", children: menuHeading }) })), searchable && (_jsx(View, { style: styles.searchContainer, children: _jsx(Input, { placeholder: searchPlaceholder, value: search, inBottomSheet: true, onChangeText: setSearch, type: "search", testID: testID ? `${testID}-search` : undefined }) })), children ? (_jsx(BottomSheetScrollView, { testID: testID ? `${testID}-options` : undefined, children: children })) : (_jsx(BottomSheetFlatList, { data: filteredOptions, keyExtractor: (option) => option.value, renderItem: renderSelectOption, ListEmptyComponent: renderEmptyComponent, testID: testID ? `${testID}-options` : undefined, ...listProps }))] }) }) })] }));
81
81
  };
82
82
  const styles = StyleSheet.create(theme => ({
83
83
  container: {
@@ -148,5 +148,9 @@ export interface SelectOptionProps {
148
148
  * Callback when this option is selected
149
149
  */
150
150
  onPress?: (value: string) => void;
151
+ /**
152
+ * Test ID for testing
153
+ */
154
+ testID?: string;
151
155
  }
152
156
  export default SelectProps;
@@ -1,6 +1,6 @@
1
1
  import { SelectOptionProps } from './Select.props';
2
2
  declare const SelectOption: {
3
- ({ label, value, leadingIcon: LeftIcon, trailingIcon: RightIcon, selected, disabled, onPress, }: SelectOptionProps): import("react/jsx-runtime").JSX.Element;
3
+ ({ label, value, leadingIcon: LeftIcon, trailingIcon: RightIcon, selected, disabled, onPress, testID, }: SelectOptionProps): import("react/jsx-runtime").JSX.Element;
4
4
  displayName: string;
5
5
  };
6
6
  export default SelectOption;
@@ -5,7 +5,7 @@ import { StyleSheet } from 'react-native-unistyles';
5
5
  import { BodyText } from '../BodyText';
6
6
  import { Icon } from '../Icon';
7
7
  import { useSelectContext } from './Select.context';
8
- const SelectOption = ({ label, value, leadingIcon: LeftIcon, trailingIcon: RightIcon, selected, disabled, onPress, }) => {
8
+ const SelectOption = ({ label, value, leadingIcon: LeftIcon, trailingIcon: RightIcon, selected, disabled, onPress, testID, }) => {
9
9
  const { selectedValue, onValueChange, close } = useSelectContext();
10
10
  const isSelected = selected !== undefined ? selected : selectedValue === value;
11
11
  styles.useVariants({ disabled });
@@ -22,7 +22,7 @@ const SelectOption = ({ label, value, leadingIcon: LeftIcon, trailingIcon: Right
22
22
  close();
23
23
  }
24
24
  };
25
- return (_jsxs(Pressable, { onPress: handlePress, disabled: disabled, style: ({ pressed }) => [styles.container, pressed && styles.pressed], children: [!!LeftIcon && (_jsx(View, { children: _jsx(Icon, { as: LeftIcon, style: styles.icon }) })), _jsx(View, { style: styles.labelContainer, children: _jsx(BodyText, { children: label }) }), isSelected && (_jsx(View, { children: _jsx(Icon, { as: TickSmallIcon, style: styles.icon }) })), !!RightIcon && !isSelected && (_jsx(View, { children: _jsx(Icon, { as: RightIcon, style: styles.icon }) }))] }));
25
+ return (_jsxs(Pressable, { onPress: handlePress, disabled: disabled, testID: testID, style: ({ pressed }) => [styles.container, pressed && styles.pressed], children: [!!LeftIcon && (_jsx(View, { children: _jsx(Icon, { as: LeftIcon, style: styles.icon }) })), _jsx(View, { style: styles.labelContainer, children: _jsx(BodyText, { children: label }) }), isSelected && (_jsx(View, { children: _jsx(Icon, { as: TickSmallIcon, style: styles.icon }) })), !!RightIcon && !isSelected && (_jsx(View, { children: _jsx(Icon, { as: RightIcon, style: styles.icon }) }))] }));
26
26
  };
27
27
  const styles = StyleSheet.create(theme => ({
28
28
  container: {
@@ -5,7 +5,7 @@ import { StyleSheet } from 'react-native-unistyles';
5
5
  import { FormField } from '../FormField';
6
6
  import { getNextIndexFromValueChange } from './VerificationInput.utils';
7
7
  import { VerificationInputSlot } from './VerificationInputSlot';
8
- 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) => {
9
9
  const length = 6;
10
10
  const inputRef = useRef(null);
11
11
  const latestValueRef = useRef(value);
@@ -160,7 +160,7 @@ const VerificationInput = forwardRef(({ value = '', onChangeText, label, labelVa
160
160
  latestSelectionRef.current = nextSelection;
161
161
  setSelection(nextSelection);
162
162
  setFocusedIndex(Math.min(nextSelection.start, length - 1));
163
- }, onFocus: handleFocus, onBlur: handleBlur, selection: selection, keyboardType: "number-pad", textContentType: "oneTimeCode", autoComplete: "sms-otp", secureTextEntry: secureTextEntry, maxLength: length, caretHidden: true, style: styles.hiddenInput }), slots.map(index => {
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 => {
164
164
  const char = displayValue[index] || '';
165
165
  const isActive = focusedIndex === index;
166
166
  const displayChar = secureTextEntry && char ? '*' : char;
@@ -168,7 +168,7 @@ const VerificationInput = forwardRef(({ value = '', onChangeText, label, labelVa
168
168
  pendingFocusIndexRef.current = index;
169
169
  inputRef.current?.focus();
170
170
  setSelectionIndex(index);
171
- } }, index));
171
+ }, testID: testID ? `${testID}-${index}` : undefined }, index));
172
172
  })] }) }));
173
173
  });
174
174
  const styles = StyleSheet.create(theme => ({
@@ -12,8 +12,8 @@ This page showcases all the components available in the Hearth React Native libr
12
12
  <AllComponents />
13
13
 
14
14
  <NextPrevPage
15
- prevLink="/?path=/docs/layout-components--docs"
15
+ prevLink="layout-components"
16
16
  prevTitle="Layout Components"
17
- nextLink="/?path=/docs/components-accordion--docs"
17
+ nextLink="components-accordion"
18
18
  nextTitle="Accordion"
19
19
  />
@@ -1,5 +1,5 @@
1
1
  import { Meta } from '@storybook/addon-docs/blocks';
2
- import { BackToTopButton } from './components';
2
+ import { BackToTopButton, NextPrevPage } from './components';
3
3
 
4
4
  <Meta title="Changelog" />
5
5
  <BackToTopButton />
@@ -9,6 +9,176 @@ 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.28.1
13
+
14
+ ### Patch Changes
15
+
16
+ - [#1018](https://github.com/utilitywarehouse/hearth/pull/1018) [`1c5e02e`](https://github.com/utilitywarehouse/hearth/commit/1c5e02ea4b61329e7c55e52f9aa4ae44abc0da23) Thanks [@fillyD](https://github.com/fillyD)! - 🐛 [FIX]: Adds missing `testID` to `Select`, `SelectOption` and `VerificationInput` component
17
+
18
+ ## 0.28.0
19
+
20
+ ### Minor Changes
21
+
22
+ - [#1014](https://github.com/utilitywarehouse/hearth/pull/1014) [`c10ff82`](https://github.com/utilitywarehouse/hearth/commit/c10ff82243265217acd95f687d48d803b3c7a4bd) Thanks [@jordmccord](https://github.com/jordmccord)! - 🌟 [FEATURE]: Add `Combobox` and `SafeAreaView` to the React Native library
23
+
24
+ The React Native package now includes a `Combobox` component for searchable selection in a bottom sheet, plus a `SafeAreaView` primitive that applies Unistyles runtime insets only when a view actually overlaps a screen edge. `Combobox` supports the built-in options API for fixed lists, controlled search values for dynamic results, and custom bottom sheet content for cases where you need to bring your own `BottomSheetFlatList` or bespoke option layout. `Modal` now uses `SafeAreaView` in its full-screen navigation modal path so content like search inputs no longer sits behind the dynamic island on iOS.
25
+
26
+ **Components affected**:
27
+ - `Combobox`
28
+ - `ComboboxOption`
29
+ - `SafeAreaView`
30
+ - `Modal`
31
+
32
+ **Developer changes**:
33
+
34
+ Import the new components from `@utilitywarehouse/hearth-react-native` and choose the API that fits your layout. Use `options` for straightforward searchable lists, render custom sheet content when you need virtualised or dynamic results, and wrap full-screen content in `SafeAreaView` when it should only pick up edge insets if it actually touches that edge.
35
+
36
+ - [`8e37595`](https://github.com/utilitywarehouse/hearth/commit/8e375958559357ce5c1703505fa7438887d9e18e) Thanks [@jordmccord](https://github.com/jordmccord)! - 🌟 [FEATURE]: Add `Pagination` component
37
+
38
+ The package now includes a `Pagination` component for moving between pages of content. It supports numbered pagination, a condensed layout for smaller spaces, optional skip buttons for jumping to the first and last page, and controlled page state so it can be wired into lists, tables, or other paged views.
39
+
40
+ **Components affected**:
41
+ - `Pagination`
42
+
43
+ **Developer changes**:
44
+
45
+ Import `Pagination` from `@utilitywarehouse/hearth-react-native` and control the current page in your screen or feature state.
46
+
47
+ ```tsx
48
+ import { useState } from 'react';
49
+ import { Pagination } from '@utilitywarehouse/hearth-react-native';
50
+
51
+ const MyComponent = () => {
52
+ const [page, setPage] = useState(1);
53
+
54
+ return <Pagination currentPage={page} onPageChange={setPage} totalPages={10} />;
55
+ };
56
+ ```
57
+
58
+ - [`8e37595`](https://github.com/utilitywarehouse/hearth/commit/8e375958559357ce5c1703505fa7438887d9e18e) Thanks [@jordmccord](https://github.com/jordmccord)! - 🌟 [FEATURE]: Add `Table` component
59
+
60
+ The package now includes a composable `Table` API for presenting structured data with headers, rows, cells, optional card-style containers, horizontal scrolling for narrow viewports, configurable column widths, and pagination support through `TablePagination`. Header cells support trailing actions such as sort controls, and the API is split into smaller building blocks so layouts can be assembled to fit different datasets.
61
+
62
+ **Components affected**:
63
+ - `Table`
64
+ - `TableHeader`
65
+ - `TableHeaderCell`
66
+ - `TableBody`
67
+ - `TableRow`
68
+ - `TableCell`
69
+ - `TablePagination`
70
+
71
+ **Developer changes**:
72
+
73
+ Import the table primitives from `@utilitywarehouse/hearth-react-native` and compose them to match your data shape. Add `columnWidths` when you need fixed or weighted columns, and pass `pagination` when rows should be paged.
74
+
75
+ ```tsx
76
+ import {
77
+ Table,
78
+ TableBody,
79
+ TableCell,
80
+ TableHeader,
81
+ TableHeaderCell,
82
+ TableRow,
83
+ } from '@utilitywarehouse/hearth-react-native';
84
+
85
+ const MyComponent = () => (
86
+ <Table columnWidths={[180, '2fr', '1fr']} container="subtle">
87
+ <TableHeader color="purple">
88
+ <TableHeaderCell>Name</TableHeaderCell>
89
+ <TableHeaderCell>Email</TableHeaderCell>
90
+ <TableHeaderCell>Status</TableHeaderCell>
91
+ </TableHeader>
92
+ <TableBody>
93
+ <TableRow>
94
+ <TableHeaderCell row>Alex Morgan</TableHeaderCell>
95
+ <TableCell>alex@example.com</TableCell>
96
+ <TableCell>Active</TableCell>
97
+ </TableRow>
98
+ </TableBody>
99
+ </Table>
100
+ );
101
+ ```
102
+
103
+ - [#1016](https://github.com/utilitywarehouse/hearth/pull/1016) [`33baa9e`](https://github.com/utilitywarehouse/hearth/commit/33baa9e8edb091bbd1d17c9a3838352a7f1b87ea) Thanks [@jordmccord](https://github.com/jordmccord)! - 🌟 [FEATURE]: Adds `Timeline` and `TimelineItem`
104
+
105
+ The package now includes `Timeline` and `TimelineItem` components for showing a sequence of static stops or progress steps. The new API supports labelled items, optional helper text, progress states for complete or active steps, and custom content within an item when you need to show extra context or actions.
106
+
107
+ **Components affected**:
108
+ - `Timeline`
109
+ - `TimelineItem`
110
+
111
+ **Developer changes**:
112
+
113
+ Import `Timeline` and `TimelineItem` from `@utilitywarehouse/hearth-react-native`. Use `variant="static"` for simple ordered events, or `variant="progress"` with item `state` values to communicate step progress.
114
+
115
+ ```tsx
116
+ import { Timeline, TimelineItem } from '@utilitywarehouse/hearth-react-native';
117
+
118
+ const MyComponent = () => (
119
+ <Timeline variant="progress">
120
+ <TimelineItem label="Ordered" helperText="We have received your order" state="complete" />
121
+ <TimelineItem label="Packed" helperText="Your items are ready" state="complete" />
122
+ <TimelineItem label="Out for delivery" helperText="Arriving today" state="active" />
123
+ <TimelineItem label="Delivered" helperText="Pending" state="incomplete" />
124
+ </Timeline>
125
+ );
126
+ ```
127
+
128
+ ## 0.27.3
129
+
130
+ ### Patch Changes
131
+
132
+ - [#1006](https://github.com/utilitywarehouse/hearth/pull/1006) [`1996112`](https://github.com/utilitywarehouse/hearth/commit/1996112864146e86972ef6b9b07a8be5a72b552f) Thanks [@jordmccord](https://github.com/jordmccord)! - 🐛 [FIX]: Make `paddingNone` remove horizontal padding for ghost buttons in `md` size
133
+
134
+ Fixed an issue where setting `paddingNone` on a ghost button did not remove horizontal padding when using `md` size. The prop now removes horizontal padding for both `sm` and `md` ghost buttons.
135
+
136
+ **Components affected**:
137
+ - `Button`
138
+
139
+ **Developer changes**:
140
+
141
+ No changes required.
142
+
143
+ - [#1012](https://github.com/utilitywarehouse/hearth/pull/1012) [`4fda116`](https://github.com/utilitywarehouse/hearth/commit/4fda116c2a1bec383df7e630180ab57166ab9da4) Thanks [@jordmccord](https://github.com/jordmccord)! - 🐛 [FIX]: Prevent outlines from being clipped for scrollable children in `Modal`
144
+
145
+ Fixed an issue in in-nav modals where child components with outlines could be visually clipped at the horizontal edges when content was scrollable.
146
+
147
+ **Components affected**:
148
+ - `Modal`
149
+
150
+ **Developer changes**:
151
+
152
+ No changes required.
153
+
154
+ - [#1012](https://github.com/utilitywarehouse/hearth/pull/1012) [`4fda116`](https://github.com/utilitywarehouse/hearth/commit/4fda116c2a1bec383df7e630180ab57166ab9da4) Thanks [@jordmccord](https://github.com/jordmccord)! - 💅 [ENHANCEMENT]: Update horizontal padding values for scrollable in-nav `Modal` content
155
+
156
+ Adjusted horizontal padding behaviour in scrollable in-nav modals to preserve child outlines while keeping visual spacing consistent.
157
+
158
+ **Components affected**:
159
+ - `Modal`
160
+
161
+ **Developer changes**:
162
+
163
+ No changes required.
164
+
165
+ - [#1009](https://github.com/utilitywarehouse/hearth/pull/1009) [`3d65ef2`](https://github.com/utilitywarehouse/hearth/commit/3d65ef2f8f7701b128a9c679f1910cd3d0f5c0c3) Thanks [@fillyD](https://github.com/fillyD)! - 🐛 [FIX]: testID for `List` component
166
+
167
+ ## 0.27.2
168
+
169
+ ### Patch Changes
170
+
171
+ - [#1003](https://github.com/utilitywarehouse/hearth/pull/1003) [`cdb95ea`](https://github.com/utilitywarehouse/hearth/commit/cdb95eabb279adaf348487ae3fb4a20e600e039e) Thanks [@jordmccord](https://github.com/jordmccord)! - 🐛 [FIX]: Correct `VerificationInput` focus progression after editing an empty slot
172
+
173
+ Fixed an issue where entering a value after selecting an empty verification slot could move focus to the wrong slot. Focus now moves to the slot immediately after the one that was actually updated.
174
+
175
+ **Components affected**:
176
+ - `VerificationInput`
177
+
178
+ **Developer changes**:
179
+
180
+ No changes required.
181
+
12
182
  ## 0.27.1
13
183
 
14
184
  ### Patch Changes
@@ -1255,3 +1425,10 @@ The changelog for the Hearth React Native library. Here you can find all the cha
1255
1425
 
1256
1426
  - [`f6366c4`](https://github.com/utilitywarehouse/hearth/commit/f6366c4da2676c535dca90be570d6e6bae5a0349) Thanks [@jordmccord](https://github.com/jordmccord)! - Initial Release 🎉
1257
1427
 
1428
+
1429
+ <NextPrevPage
1430
+ prevLink="getting-started"
1431
+ prevTitle="Getting Started"
1432
+ nextLink="styling"
1433
+ nextTitle="Styling"
1434
+ />