@utilitywarehouse/hearth-react-native 0.27.2-test → 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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +15 -0
- package/build/components/Button/ButtonRoot.js +8 -0
- 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/Modal/Modal.js +16 -11
- package/build/components/VerificationInput/VerificationInput.js +2 -2
- package/package.json +4 -4
- package/src/components/Button/Button.stories.tsx +43 -7
- package/src/components/Button/ButtonRoot.tsx +8 -0
- package/src/components/Modal/Modal.tsx +31 -16
- package/src/components/VerificationInput/VerificationInput.tsx +2 -0
package/.turbo/turbo-build.log
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @utilitywarehouse/hearth-react-native
|
|
2
2
|
|
|
3
|
+
## 0.27.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#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
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
**Components affected**:
|
|
12
|
+
- `VerificationInput`
|
|
13
|
+
|
|
14
|
+
**Developer changes**:
|
|
15
|
+
|
|
16
|
+
No changes required.
|
|
17
|
+
|
|
3
18
|
## 0.27.1
|
|
4
19
|
|
|
5
20
|
### Patch Changes
|
|
@@ -108,6 +108,14 @@ const styles = StyleSheet.create(theme => ({
|
|
|
108
108
|
paddingHorizontal: 0,
|
|
109
109
|
},
|
|
110
110
|
},
|
|
111
|
+
{
|
|
112
|
+
size: 'md',
|
|
113
|
+
paddingNone: true,
|
|
114
|
+
variant: 'ghost',
|
|
115
|
+
styles: {
|
|
116
|
+
paddingHorizontal: 0,
|
|
117
|
+
},
|
|
118
|
+
},
|
|
111
119
|
// Variant Color Schemes
|
|
112
120
|
// Emphasis
|
|
113
121
|
// Emphasis Yellow
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import dayjs from 'dayjs';
|
|
3
|
+
import { useCallback, useMemo } from 'react';
|
|
4
|
+
import { ScrollView, View } from 'react-native';
|
|
5
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
6
|
+
import { BodyText } from '../BodyText';
|
|
7
|
+
import { useDatePickerContext } from './DatePicker.context';
|
|
8
|
+
import { CONTAINER_HEIGHT } from './enums';
|
|
9
|
+
import PeriodPicker from './time-picker/period-picker';
|
|
10
|
+
import Wheel from './time-picker/wheel';
|
|
11
|
+
import { formatNumber, getParsedDate } from './utils';
|
|
12
|
+
const createNumberList = (num, numerals, startFrom = 0) => {
|
|
13
|
+
return Array.from({ length: num }, (_, i) => ({
|
|
14
|
+
value: i + startFrom,
|
|
15
|
+
text: i + startFrom < 10
|
|
16
|
+
? `${formatNumber(0, numerals)}${formatNumber(i + startFrom, numerals)}`
|
|
17
|
+
: `${formatNumber(i + startFrom, numerals)}`,
|
|
18
|
+
}));
|
|
19
|
+
};
|
|
20
|
+
const TimePicker = () => {
|
|
21
|
+
const { currentDate, date, onSelectDate, timeZone, numerals = 'latn', use12Hours, } = useDatePickerContext();
|
|
22
|
+
const hours = useMemo(() => createNumberList(use12Hours ? 12 : 24, numerals, use12Hours ? 1 : 0), [numerals, use12Hours]);
|
|
23
|
+
const minutes = useMemo(() => createNumberList(60, numerals), [numerals]);
|
|
24
|
+
const { hour, hour12, minute, period } = getParsedDate(date || currentDate);
|
|
25
|
+
const handleChangeHour = useCallback((value) => {
|
|
26
|
+
let hour24 = value;
|
|
27
|
+
if (use12Hours) {
|
|
28
|
+
if (period === 'AM' && value === 12) {
|
|
29
|
+
hour24 = 0;
|
|
30
|
+
}
|
|
31
|
+
else if (period === 'PM' && value < 12) {
|
|
32
|
+
hour24 = value + 12;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
hour24 = value;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const newDate = dayjs.tz(date, timeZone).hour(hour24).minute(minute);
|
|
39
|
+
onSelectDate(newDate);
|
|
40
|
+
}, [date, onSelectDate, timeZone, use12Hours, period, minute]);
|
|
41
|
+
const handleChangeMinute = useCallback((value) => {
|
|
42
|
+
const newDate = dayjs.tz(date, timeZone).minute(value);
|
|
43
|
+
onSelectDate(newDate);
|
|
44
|
+
}, [date, onSelectDate, timeZone]);
|
|
45
|
+
const handlePeriodChange = useCallback((newPeriod) => {
|
|
46
|
+
let newHour = hour12;
|
|
47
|
+
if (newPeriod === 'PM' && hour12 < 12) {
|
|
48
|
+
newHour = hour12 + 12;
|
|
49
|
+
}
|
|
50
|
+
else if (newPeriod === 'AM' && hour12 === 12) {
|
|
51
|
+
newHour = 0;
|
|
52
|
+
}
|
|
53
|
+
else if (newPeriod === 'AM' && hour >= 12) {
|
|
54
|
+
newHour = hour12;
|
|
55
|
+
}
|
|
56
|
+
const newDate = dayjs.tz(date || currentDate, timeZone).hour(newHour);
|
|
57
|
+
onSelectDate(newDate);
|
|
58
|
+
}, [date, currentDate, onSelectDate, timeZone, hour, hour12]);
|
|
59
|
+
return (_jsxs(ScrollView, { horizontal: true, scrollEnabled: false, contentContainerStyle: styles.container, testID: "time-selector", children: [_jsxs(View, { style: styles.timePickerContainer, children: [_jsx(View, { style: styles.wheelContainer, children: _jsx(Wheel, { value: use12Hours ? hour12 : hour, items: hours, setValue: handleChangeHour }) }), _jsx(BodyText, { style: styles.timeSeparator, children: ":" }), _jsx(View, { style: styles.wheelContainer, children: _jsx(Wheel, { value: minute, items: minutes, setValue: handleChangeMinute }) })] }), use12Hours && period ? (_jsx(View, { style: styles.periodContainer, children: _jsx(PeriodPicker, { value: period, setValue: handlePeriodChange }) })) : null] }));
|
|
60
|
+
};
|
|
61
|
+
const styles = StyleSheet.create({
|
|
62
|
+
container: {
|
|
63
|
+
flex: 1,
|
|
64
|
+
alignItems: 'center',
|
|
65
|
+
justifyContent: 'center',
|
|
66
|
+
},
|
|
67
|
+
wheelContainer: {
|
|
68
|
+
flex: 1,
|
|
69
|
+
},
|
|
70
|
+
timePickerContainer: {
|
|
71
|
+
alignItems: 'center',
|
|
72
|
+
justifyContent: 'center',
|
|
73
|
+
width: CONTAINER_HEIGHT / 2,
|
|
74
|
+
height: CONTAINER_HEIGHT / 2,
|
|
75
|
+
flexDirection: 'row',
|
|
76
|
+
},
|
|
77
|
+
timeSeparator: {
|
|
78
|
+
marginHorizontal: 5,
|
|
79
|
+
},
|
|
80
|
+
periodContainer: {
|
|
81
|
+
marginLeft: 10,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
export default TimePicker;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Animated } from 'react-native';
|
|
2
|
+
const FACTORIAL_3 = 3 * 2;
|
|
3
|
+
const FACTORIAL_5 = 5 * 4 * FACTORIAL_3;
|
|
4
|
+
const FACTORIAL_7 = 7 * 6 * FACTORIAL_5;
|
|
5
|
+
function sin(animated) {
|
|
6
|
+
const normalized = normalize(animated);
|
|
7
|
+
const square = Animated.multiply(normalized, normalized);
|
|
8
|
+
const pow3 = Animated.multiply(normalized, square);
|
|
9
|
+
const pow5 = Animated.multiply(pow3, square);
|
|
10
|
+
const pow7 = Animated.multiply(pow5, square);
|
|
11
|
+
return Animated.add(Animated.add(normalized, Animated.multiply(pow3, -1 / FACTORIAL_3)), Animated.add(Animated.multiply(pow5, 1 / FACTORIAL_5), Animated.multiply(pow7, -1 / FACTORIAL_7)));
|
|
12
|
+
}
|
|
13
|
+
function normalize(animated) {
|
|
14
|
+
return Animated.add(Animated.modulo(Animated.add(animated, Math.PI), Math.PI * 2), -Math.PI).interpolate({
|
|
15
|
+
inputRange: [-Math.PI, -Math.PI / 2, Math.PI / 2, Math.PI],
|
|
16
|
+
outputRange: [0, -Math.PI / 2, Math.PI / 2, 0],
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export { sin, normalize };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { memo } from 'react';
|
|
3
|
+
import WheelPicker from './wheel-picker';
|
|
4
|
+
const options = [
|
|
5
|
+
{ value: 'AM', text: 'AM' },
|
|
6
|
+
{ value: 'PM', text: 'PM' },
|
|
7
|
+
];
|
|
8
|
+
const PeriodNative = ({ value, setValue = () => { } }) => {
|
|
9
|
+
return (_jsx(WheelPicker, { value: value, options: options, onChange: setValue,
|
|
10
|
+
//containerStyle={defaultStyles.container}
|
|
11
|
+
itemHeight: 44, decelerationRate: "fast" }));
|
|
12
|
+
};
|
|
13
|
+
const customComparator = (prev, next) => {
|
|
14
|
+
const areEqual = prev.value === next.value && prev.setValue === next.setValue;
|
|
15
|
+
return areEqual;
|
|
16
|
+
};
|
|
17
|
+
export default memo(PeriodNative, customComparator);
|
|
@@ -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 PeriodNative from './period-native';
|
|
5
|
+
import PeriodWeb from './period-web';
|
|
6
|
+
const PeriodPicker = (props) => {
|
|
7
|
+
const Component = Platform.OS === 'web' ? PeriodWeb : PeriodNative;
|
|
8
|
+
return _jsx(Component, { ...props });
|
|
9
|
+
};
|
|
10
|
+
export default memo(PeriodPicker);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { memo } from 'react';
|
|
3
|
+
import { Pressable, View } from 'react-native';
|
|
4
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
5
|
+
import { BodyText } from '../../BodyText';
|
|
6
|
+
const PeriodWeb = ({ value, setValue = () => { } }) => {
|
|
7
|
+
return (_jsx(Pressable, { onPress: () => setValue(value == 'AM' ? 'PM' : 'AM'), children: _jsx(View, { style: [styles.period], children: _jsx(BodyText, { children: value }) }) }));
|
|
8
|
+
};
|
|
9
|
+
const styles = StyleSheet.create({
|
|
10
|
+
period: {
|
|
11
|
+
width: 65,
|
|
12
|
+
height: 44,
|
|
13
|
+
alignItems: 'center',
|
|
14
|
+
justifyContent: 'center',
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
const customComparator = (prev, next) => {
|
|
18
|
+
const areEqual = prev.value === next.value && prev.setValue === next.setValue;
|
|
19
|
+
return areEqual;
|
|
20
|
+
};
|
|
21
|
+
export default memo(PeriodWeb, customComparator);
|
|
@@ -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,19 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { memo } from 'react';
|
|
3
|
+
import { Platform } from 'react-native';
|
|
4
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
5
|
+
import WheelPicker from './wheel-picker';
|
|
6
|
+
const WheelNative = ({ value, setValue = () => { }, items }) => {
|
|
7
|
+
return (_jsx(WheelPicker, { value: value, options: items, onChange: setValue, containerStyle: styles.container, itemHeight: 44, decelerationRate: "fast" }));
|
|
8
|
+
};
|
|
9
|
+
const styles = StyleSheet.create({
|
|
10
|
+
container: {
|
|
11
|
+
display: 'flex',
|
|
12
|
+
...Platform.select({
|
|
13
|
+
web: {
|
|
14
|
+
userSelect: 'none',
|
|
15
|
+
},
|
|
16
|
+
}),
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
export default memo(WheelNative);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Animated, StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import { PickerOption } from '../../DatePicker.props';
|
|
4
|
+
interface ItemProps {
|
|
5
|
+
style: StyleProp<ViewStyle>;
|
|
6
|
+
option: PickerOption | null;
|
|
7
|
+
height: number;
|
|
8
|
+
index: number;
|
|
9
|
+
currentScrollIndex: Animated.AnimatedAddition<number>;
|
|
10
|
+
visibleRest: number;
|
|
11
|
+
rotationFunction: (x: number) => number;
|
|
12
|
+
opacityFunction: (x: number) => number;
|
|
13
|
+
scaleFunction: (x: number) => number;
|
|
14
|
+
}
|
|
15
|
+
declare const _default: React.NamedExoticComponent<ItemProps>;
|
|
16
|
+
export default _default;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Animated } from 'react-native';
|
|
4
|
+
import { BodyText } from '../../../BodyText';
|
|
5
|
+
import styles from './wheel-picker.style';
|
|
6
|
+
const WheelPickerItem = ({ style, height, option, index, visibleRest, currentScrollIndex, opacityFunction, rotationFunction, scaleFunction, }) => {
|
|
7
|
+
const relativeScrollIndex = Animated.subtract(index, currentScrollIndex);
|
|
8
|
+
const translateY = relativeScrollIndex.interpolate({
|
|
9
|
+
inputRange: (() => {
|
|
10
|
+
const range = [0];
|
|
11
|
+
for (let i = 1; i <= visibleRest + 1; i++) {
|
|
12
|
+
range.unshift(-i);
|
|
13
|
+
range.push(i);
|
|
14
|
+
}
|
|
15
|
+
return range;
|
|
16
|
+
})(),
|
|
17
|
+
outputRange: (() => {
|
|
18
|
+
const range = [0];
|
|
19
|
+
for (let i = 1; i <= visibleRest + 1; i++) {
|
|
20
|
+
let y = (height / 2) * (1 - Math.sin(Math.PI / 2 - rotationFunction(i)));
|
|
21
|
+
for (let j = 1; j < i; j++) {
|
|
22
|
+
y += height * (1 - Math.sin(Math.PI / 2 - rotationFunction(j)));
|
|
23
|
+
}
|
|
24
|
+
range.unshift(y);
|
|
25
|
+
range.push(-y);
|
|
26
|
+
}
|
|
27
|
+
return range;
|
|
28
|
+
})(),
|
|
29
|
+
});
|
|
30
|
+
const opacity = relativeScrollIndex.interpolate({
|
|
31
|
+
inputRange: (() => {
|
|
32
|
+
const range = [0];
|
|
33
|
+
for (let i = 1; i <= visibleRest + 1; i++) {
|
|
34
|
+
range.unshift(-i);
|
|
35
|
+
range.push(i);
|
|
36
|
+
}
|
|
37
|
+
return range;
|
|
38
|
+
})(),
|
|
39
|
+
outputRange: (() => {
|
|
40
|
+
const range = [1];
|
|
41
|
+
for (let x = 1; x <= visibleRest + 1; x++) {
|
|
42
|
+
const y = opacityFunction(x);
|
|
43
|
+
range.unshift(y);
|
|
44
|
+
range.push(y);
|
|
45
|
+
}
|
|
46
|
+
return range;
|
|
47
|
+
})(),
|
|
48
|
+
});
|
|
49
|
+
const scale = relativeScrollIndex.interpolate({
|
|
50
|
+
inputRange: (() => {
|
|
51
|
+
const range = [0];
|
|
52
|
+
for (let i = 1; i <= visibleRest + 1; i++) {
|
|
53
|
+
range.unshift(-i);
|
|
54
|
+
range.push(i);
|
|
55
|
+
}
|
|
56
|
+
return range;
|
|
57
|
+
})(),
|
|
58
|
+
outputRange: (() => {
|
|
59
|
+
const range = [1.0];
|
|
60
|
+
for (let x = 1; x <= visibleRest + 1; x++) {
|
|
61
|
+
const y = scaleFunction(x);
|
|
62
|
+
range.unshift(y);
|
|
63
|
+
range.push(y);
|
|
64
|
+
}
|
|
65
|
+
return range;
|
|
66
|
+
})(),
|
|
67
|
+
});
|
|
68
|
+
const rotateX = relativeScrollIndex.interpolate({
|
|
69
|
+
inputRange: (() => {
|
|
70
|
+
const range = [0];
|
|
71
|
+
for (let i = 1; i <= visibleRest + 1; i++) {
|
|
72
|
+
range.unshift(-i);
|
|
73
|
+
range.push(i);
|
|
74
|
+
}
|
|
75
|
+
return range;
|
|
76
|
+
})(),
|
|
77
|
+
outputRange: (() => {
|
|
78
|
+
const range = ['0deg'];
|
|
79
|
+
for (let x = 1; x <= visibleRest + 1; x++) {
|
|
80
|
+
const y = rotationFunction(x);
|
|
81
|
+
range.unshift(`${y}deg`);
|
|
82
|
+
range.push(`${y}deg`);
|
|
83
|
+
}
|
|
84
|
+
return range;
|
|
85
|
+
})(),
|
|
86
|
+
});
|
|
87
|
+
return (_jsx(Animated.View, { style: [
|
|
88
|
+
styles.option,
|
|
89
|
+
style,
|
|
90
|
+
{
|
|
91
|
+
height,
|
|
92
|
+
opacity,
|
|
93
|
+
transform: [{ translateY }, { rotateX }, { scale }],
|
|
94
|
+
},
|
|
95
|
+
], children: _jsx(BodyText, { children: option?.text }) }));
|
|
96
|
+
};
|
|
97
|
+
export default React.memo(WheelPickerItem);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { FlatListProps, StyleProp, ViewProps, ViewStyle } from 'react-native';
|
|
3
|
+
import { PickerOption } from '../../DatePicker.props';
|
|
4
|
+
interface Props {
|
|
5
|
+
value: number | string;
|
|
6
|
+
options: PickerOption[];
|
|
7
|
+
onChange: (index: number | string) => void;
|
|
8
|
+
selectedIndicatorStyle?: StyleProp<ViewStyle>;
|
|
9
|
+
itemStyle?: ViewStyle;
|
|
10
|
+
itemHeight?: number;
|
|
11
|
+
containerStyle?: ViewStyle;
|
|
12
|
+
containerProps?: Omit<ViewProps, 'style'>;
|
|
13
|
+
scaleFunction?: (x: number) => number;
|
|
14
|
+
rotationFunction?: (x: number) => number;
|
|
15
|
+
opacityFunction?: (x: number) => number;
|
|
16
|
+
visibleRest?: number;
|
|
17
|
+
decelerationRate?: 'normal' | 'fast' | number;
|
|
18
|
+
flatListProps?: Omit<FlatListProps<PickerOption | null>, 'data' | 'renderItem'>;
|
|
19
|
+
}
|
|
20
|
+
declare const _default: React.NamedExoticComponent<Props>;
|
|
21
|
+
export default _default;
|
|
@@ -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);
|
|
@@ -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: {
|
|
@@ -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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@utilitywarehouse/hearth-react-native",
|
|
3
|
-
"version": "0.27.2-
|
|
3
|
+
"version": "0.27.2-testid-fix-1",
|
|
4
4
|
"description": "Utility Warehouse React Native UI library",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -56,11 +56,11 @@
|
|
|
56
56
|
"vite": "^7.1.3",
|
|
57
57
|
"vite-plugin-svgr": "^4.5.0",
|
|
58
58
|
"vitest": "^3.2.4",
|
|
59
|
-
"@utilitywarehouse/hearth-svg-assets": "^0.5.0",
|
|
60
59
|
"@utilitywarehouse/hearth-react-native-icons": "^0.8.0",
|
|
61
|
-
"@utilitywarehouse/hearth-
|
|
60
|
+
"@utilitywarehouse/hearth-tokens": "^0.2.4",
|
|
62
61
|
"@utilitywarehouse/hearth-fonts": "^0.0.4",
|
|
63
|
-
"@utilitywarehouse/hearth-
|
|
62
|
+
"@utilitywarehouse/hearth-react-icons": "^0.8.0",
|
|
63
|
+
"@utilitywarehouse/hearth-svg-assets": "^0.5.0"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
|
66
66
|
"@gorhom/bottom-sheet": "^5.0.0",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Meta, StoryObj } from '@storybook/react-
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react-native';
|
|
2
2
|
import * as Icons from '@utilitywarehouse/hearth-react-native-icons';
|
|
3
3
|
import { AddSmallIcon, ChevronRightSmallIcon } from '@utilitywarehouse/hearth-react-native-icons';
|
|
4
4
|
import { Platform } from 'react-native';
|
|
@@ -92,7 +92,7 @@ type Story = StoryObj<typeof meta>;
|
|
|
92
92
|
|
|
93
93
|
export const Playground: Story = {
|
|
94
94
|
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
|
|
95
|
-
render: ({ icon: _icon, children: _, ...args }) => {
|
|
95
|
+
render: ({ icon: _icon, children: _, ...args }: StoryObj<typeof meta.args>) => {
|
|
96
96
|
// @ts-expect-error - This is a playground
|
|
97
97
|
const icon = _icon === 'none' ? undefined : Icons[_icon];
|
|
98
98
|
return <Button {...args} icon={icon} />;
|
|
@@ -104,7 +104,7 @@ export const Variants: Story = {
|
|
|
104
104
|
controls: { exclude: ['variant'] },
|
|
105
105
|
},
|
|
106
106
|
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
|
|
107
|
-
render: ({ icon: _icon, children: _, ...args }) => {
|
|
107
|
+
render: ({ icon: _icon, children: _, ...args }: StoryObj<typeof meta.args>) => {
|
|
108
108
|
// @ts-expect-error - This is a playground
|
|
109
109
|
const icon = _icon === 'none' ? undefined : Icons[_icon];
|
|
110
110
|
return (
|
|
@@ -123,11 +123,9 @@ export const Variants: Story = {
|
|
|
123
123
|
{args.colorScheme !== 'highlight' && (
|
|
124
124
|
<>
|
|
125
125
|
<VariantTitle title="Outline" invert={args.inverted}>
|
|
126
|
-
{/* @ts-expect-error - story loop types don't match */}
|
|
127
126
|
<Button {...args} variant="outline" icon={icon} />
|
|
128
127
|
</VariantTitle>
|
|
129
128
|
<VariantTitle title="Ghost" invert={args.inverted}>
|
|
130
|
-
{/* @ts-expect-error - story loop types don't match */}
|
|
131
129
|
<Button {...args} variant="ghost" icon={icon} />
|
|
132
130
|
</VariantTitle>
|
|
133
131
|
</>
|
|
@@ -138,6 +136,44 @@ export const Variants: Story = {
|
|
|
138
136
|
},
|
|
139
137
|
};
|
|
140
138
|
|
|
139
|
+
export const PaddingNone: Story = {
|
|
140
|
+
parameters: {
|
|
141
|
+
controls: {
|
|
142
|
+
include: ['text', 'size', 'inverted', 'icon', 'iconPosition'],
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
|
|
146
|
+
render: ({ icon: _icon, children: _, ...args }: StoryObj<typeof meta.args>) => {
|
|
147
|
+
// @ts-expect-error - This is a playground
|
|
148
|
+
const icon = _icon === 'none' ? undefined : Icons[_icon];
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<Flex direction="column" spacing="lg">
|
|
152
|
+
<VariantTitle title="Default Padding" invert={args.inverted}>
|
|
153
|
+
<Flex direction="row" align="center" spacing="none">
|
|
154
|
+
<Box backgroundColor="brand" width="100" height="100" />
|
|
155
|
+
<Button
|
|
156
|
+
{...args}
|
|
157
|
+
colorScheme="functional"
|
|
158
|
+
variant="ghost"
|
|
159
|
+
icon={icon}
|
|
160
|
+
paddingNone={false}
|
|
161
|
+
/>
|
|
162
|
+
<Box backgroundColor="brand" width="100" height="100" />
|
|
163
|
+
</Flex>
|
|
164
|
+
</VariantTitle>
|
|
165
|
+
<VariantTitle title="No Padding (paddingNone)" invert={args.inverted}>
|
|
166
|
+
<Flex direction="row" align="center" spacing="none">
|
|
167
|
+
<Box backgroundColor="brand" width="100" height="100" />
|
|
168
|
+
<Button {...args} colorScheme="functional" variant="ghost" icon={icon} paddingNone />
|
|
169
|
+
<Box backgroundColor="brand" width="100" height="100" />
|
|
170
|
+
</Flex>
|
|
171
|
+
</VariantTitle>
|
|
172
|
+
</Flex>
|
|
173
|
+
);
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
|
|
141
177
|
type ColorScheme = ButtonProps['colorScheme'];
|
|
142
178
|
type Variant = ButtonProps['variant'];
|
|
143
179
|
|
|
@@ -145,7 +181,7 @@ export const KitchenSink: Story = {
|
|
|
145
181
|
parameters: {
|
|
146
182
|
controls: { include: ['text', 'size', 'inverted'] },
|
|
147
183
|
},
|
|
148
|
-
render: ({ text, inverted, size }) => {
|
|
184
|
+
render: ({ text, inverted, size }: StoryObj<typeof meta.args>) => {
|
|
149
185
|
const schemes: Array<ColorScheme> = ['highlight', 'destructive', 'affirmative', 'functional'];
|
|
150
186
|
const variants: Array<Variant> = ['emphasis', 'solid', 'outline', 'ghost'];
|
|
151
187
|
return (
|
|
@@ -174,7 +210,7 @@ export const KitchenSink: Story = {
|
|
|
174
210
|
.map(variant => (
|
|
175
211
|
<Box key={variant} mb="100">
|
|
176
212
|
<Box mb="100">
|
|
177
|
-
<DetailText size="lg"
|
|
213
|
+
<DetailText size="lg" inverted={inverted}>
|
|
178
214
|
{scheme} - {variant}
|
|
179
215
|
</DetailText>
|
|
180
216
|
</Box>
|
|
@@ -132,6 +132,14 @@ const styles = StyleSheet.create(theme => ({
|
|
|
132
132
|
paddingHorizontal: 0,
|
|
133
133
|
},
|
|
134
134
|
},
|
|
135
|
+
{
|
|
136
|
+
size: 'md',
|
|
137
|
+
paddingNone: true,
|
|
138
|
+
variant: 'ghost',
|
|
139
|
+
styles: {
|
|
140
|
+
paddingHorizontal: 0,
|
|
141
|
+
},
|
|
142
|
+
},
|
|
135
143
|
// Variant Color Schemes
|
|
136
144
|
// Emphasis
|
|
137
145
|
// Emphasis Yellow
|
|
@@ -7,7 +7,14 @@ import {
|
|
|
7
7
|
import { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/types';
|
|
8
8
|
import { CloseMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
|
|
9
9
|
import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
AccessibilityInfo,
|
|
12
|
+
Dimensions,
|
|
13
|
+
Platform,
|
|
14
|
+
ScrollView,
|
|
15
|
+
View,
|
|
16
|
+
findNodeHandle,
|
|
17
|
+
} from 'react-native';
|
|
11
18
|
import Animated, {
|
|
12
19
|
Easing,
|
|
13
20
|
useAnimatedStyle,
|
|
@@ -292,13 +299,23 @@ const Modal = ({
|
|
|
292
299
|
</View>
|
|
293
300
|
) : null}
|
|
294
301
|
{inNavModal && (
|
|
295
|
-
<InNavModalContainer
|
|
302
|
+
<InNavModalContainer
|
|
303
|
+
style={{
|
|
304
|
+
flex: stickyFooter ? 1 : 0,
|
|
305
|
+
...(scrollable ? { marginHorizontal: -1 } : {}),
|
|
306
|
+
}}
|
|
307
|
+
{...(scrollable ? { contentContainerStyle: { paddingHorizontal: 1 } } : {})}
|
|
308
|
+
>
|
|
296
309
|
{children}
|
|
297
|
-
{!stickyFooter ?
|
|
310
|
+
{!stickyFooter ? (
|
|
311
|
+
<View style={styles.inNavModalFooterContainer}>{footer}</View>
|
|
312
|
+
) : null}
|
|
298
313
|
</InNavModalContainer>
|
|
299
314
|
)}
|
|
300
315
|
{!inNavModal && children}
|
|
301
|
-
{((!stickyFooter && !inNavModal) || (inNavModal && stickyFooter)) && !noButtons
|
|
316
|
+
{((!stickyFooter && !inNavModal) || (inNavModal && stickyFooter)) && !noButtons
|
|
317
|
+
? footer
|
|
318
|
+
: null}
|
|
302
319
|
</View>
|
|
303
320
|
)}
|
|
304
321
|
</>
|
|
@@ -322,7 +339,7 @@ const Modal = ({
|
|
|
322
339
|
|
|
323
340
|
return inNavModal ? (
|
|
324
341
|
<View
|
|
325
|
-
onLayout={
|
|
342
|
+
onLayout={e => {
|
|
326
343
|
setInNavModalHeight(e.nativeEvent.layout.height);
|
|
327
344
|
}}
|
|
328
345
|
style={{
|
|
@@ -338,9 +355,7 @@ const Modal = ({
|
|
|
338
355
|
<Animated.View
|
|
339
356
|
style={[styles.inNavModalContainer, Platform.OS === 'android' && animatedInNavModalStyle]}
|
|
340
357
|
>
|
|
341
|
-
<View style={styles.inNavModalContent}>
|
|
342
|
-
{content}
|
|
343
|
-
</View>
|
|
358
|
+
<View style={styles.inNavModalContent}>{content}</View>
|
|
344
359
|
</Animated.View>
|
|
345
360
|
</View>
|
|
346
361
|
) : (
|
|
@@ -471,7 +486,7 @@ const styles = StyleSheet.create((theme, rt) => ({
|
|
|
471
486
|
borderTopLeftRadius: theme.components.modal.borderRadius,
|
|
472
487
|
borderTopRightRadius: theme.components.modal.borderRadius,
|
|
473
488
|
backgroundColor: theme.color.surface.neutral.strong,
|
|
474
|
-
paddingBottom: theme.components.
|
|
489
|
+
paddingBottom: theme.components.bottomSheet.padding + rt.insets.bottom,
|
|
475
490
|
variants: {
|
|
476
491
|
background: {
|
|
477
492
|
primary: {},
|
|
@@ -481,22 +496,22 @@ const styles = StyleSheet.create((theme, rt) => ({
|
|
|
481
496
|
},
|
|
482
497
|
fullscreen: {
|
|
483
498
|
true: {
|
|
484
|
-
padding: theme.components.
|
|
499
|
+
padding: theme.components.bottomSheet.padding,
|
|
485
500
|
paddingTop: rt.insets.top,
|
|
486
501
|
},
|
|
487
502
|
false: {
|
|
488
|
-
padding: theme.components.
|
|
489
|
-
}
|
|
490
|
-
}
|
|
503
|
+
padding: theme.components.bottomSheet.padding,
|
|
504
|
+
},
|
|
505
|
+
},
|
|
491
506
|
},
|
|
492
507
|
},
|
|
493
508
|
inNavModalFooterContainer: {
|
|
494
|
-
paddingTop: theme.components.
|
|
509
|
+
paddingTop: theme.components.bottomSheet.padding,
|
|
495
510
|
},
|
|
496
511
|
androidContainer: {
|
|
497
512
|
height: rt.insets.top + 18,
|
|
498
|
-
paddingLeft: theme.components.
|
|
499
|
-
paddingRight: theme.components.
|
|
513
|
+
paddingLeft: theme.components.bottomSheet.padding,
|
|
514
|
+
paddingRight: theme.components.bottomSheet.padding,
|
|
500
515
|
justifyContent: 'flex-end',
|
|
501
516
|
},
|
|
502
517
|
pretendContent: {
|
|
@@ -23,6 +23,7 @@ const VerificationInput = forwardRef<VerificationInputHandle, VerificationInputP
|
|
|
23
23
|
secureTextEntry = false,
|
|
24
24
|
autoFocus = false,
|
|
25
25
|
style,
|
|
26
|
+
testID,
|
|
26
27
|
...props
|
|
27
28
|
},
|
|
28
29
|
ref
|
|
@@ -239,6 +240,7 @@ const VerificationInput = forwardRef<VerificationInputHandle, VerificationInputP
|
|
|
239
240
|
maxLength={length}
|
|
240
241
|
caretHidden
|
|
241
242
|
style={styles.hiddenInput}
|
|
243
|
+
testID={testID}
|
|
242
244
|
/>
|
|
243
245
|
{slots.map(index => {
|
|
244
246
|
const char = displayValue[index] || '';
|