@widergy/mobile-ui 1.31.2 → 1.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/lib/components/UTRating/README.md +44 -0
- package/lib/components/UTRating/components/Option/index.js +32 -0
- package/lib/components/UTRating/components/Option/utils.js +17 -0
- package/lib/components/UTRating/constants.js +5 -0
- package/lib/components/UTRating/index.js +101 -0
- package/lib/components/UTRating/styles.js +38 -0
- package/lib/components/UTTracker/components/Step/components/BarMask/index.js +54 -0
- package/lib/components/UTTracker/components/Step/components/BarMask/styles.js +31 -0
- package/lib/components/UTTracker/components/Step/components/StepMask/index.js +84 -0
- package/lib/components/UTTracker/components/Step/components/StepMask/styles.js +38 -0
- package/lib/components/UTTracker/components/Step/constants.js +10 -0
- package/lib/components/UTTracker/components/Step/index.js +58 -13
- package/lib/components/UTTracker/components/Step/styles.js +0 -6
- package/lib/components/UTTracker/index.js +1 -0
- package/lib/index.js +1 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [1.32.0](https://github.com/widergy/mobile-ui/compare/v1.31.3...v1.32.0) (2024-11-06)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* [UGC-924] animations ut tracker ([#386](https://github.com/widergy/mobile-ui/issues/386)) ([8032b66](https://github.com/widergy/mobile-ui/commit/8032b66d2c3712672cc7bd5391f4aac81a9a3aca))
|
|
7
|
+
|
|
8
|
+
## [1.31.3](https://github.com/widergy/mobile-ui/compare/v1.31.2...v1.31.3) (2024-11-05)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* [EVE-4328] UTRating ([#384](https://github.com/widergy/mobile-ui/issues/384)) ([6485384](https://github.com/widergy/mobile-ui/commit/6485384e2fed5cef0c2c68b88b1cbf8e45d4f3c7))
|
|
14
|
+
|
|
1
15
|
## [1.31.2](https://github.com/widergy/mobile-ui/compare/v1.31.1...v1.31.2) (2024-11-05)
|
|
2
16
|
|
|
3
17
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# UTRating
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
`UTRating` is a configurable rating input component that enables users to easily provide feedback through visual indicators.
|
|
6
|
+
|
|
7
|
+
## Props
|
|
8
|
+
|
|
9
|
+
| Name | Type | Default | Description |
|
|
10
|
+
| ------------------- | --------- | ------- | -------------------------------------------------------------------------------------------------------- |
|
|
11
|
+
| classNames | object | | Custom CSS class names for styling the component. |
|
|
12
|
+
| dataTestId | string | | Unique identifier for testing purposes. |
|
|
13
|
+
| disabled | bool | | Indicates whether the input is disabled and cannot be interacted with. |
|
|
14
|
+
| error | string | | Error message to display when validation fails or an issue occurs. |
|
|
15
|
+
| helpTextEnd | string | | Additional help text displayed at the end of the input for user guidance. |
|
|
16
|
+
| helpTextStart | string | | Additional help text displayed at the beginning of the input for user guidance. |
|
|
17
|
+
| onChange | func | | Callback function invoked when the input value changes. |
|
|
18
|
+
| options | array | | Array of options available for selection. |
|
|
19
|
+
| required | bool | | Indicates whether the input is mandatory for form submission. |
|
|
20
|
+
| title | string | | Title for the input, providing context to the user. |
|
|
21
|
+
| validations | array | | Array of validation rules to be applied to the input value. |
|
|
22
|
+
| value | string | | The current value of the input. |
|
|
23
|
+
| variant | string | `star` | Defines the visual style component. |
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### options
|
|
27
|
+
|
|
28
|
+
`options` is an array of objects with a `value` and a `name` key. The values should be in the range of 0-10, except for the `faces` variant. In desktop view, all options are displayed in one row; in responsive view, they are broken into 2 rows if there are more than 5 options.
|
|
29
|
+
|
|
30
|
+
### variant
|
|
31
|
+
|
|
32
|
+
The value of `variant` must be one of the following:
|
|
33
|
+
|
|
34
|
+
- `faces`: the options are represented as face icons. Option values should exclusively be between 1 and 5, values outside this range are not supported.
|
|
35
|
+
- `star`: the options are represented as star icons.
|
|
36
|
+
- `text`: the options are represented as the option's `text`.
|
|
37
|
+
|
|
38
|
+
### Structure of Validations
|
|
39
|
+
|
|
40
|
+
For detailed information about the structure of validations, please refer to the UTValidation component documentation.
|
|
41
|
+
|
|
42
|
+
### Handling Errors
|
|
43
|
+
|
|
44
|
+
Errors can be displayed below the text input using either the `error` prop or the `validations` prop.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { bool, func, object, string } from 'prop-types';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import { RATING_VARIANTS } from '../../constants';
|
|
5
|
+
import UTButton from '../../../UTButton';
|
|
6
|
+
|
|
7
|
+
import { getIcon, getVariant } from './utils';
|
|
8
|
+
|
|
9
|
+
const Option = ({ disabled, isSelected, name, onChange, value, variant, wrapperStyle }) => (
|
|
10
|
+
<UTButton
|
|
11
|
+
colorTheme={isSelected ? 'primary' : 'secondary'}
|
|
12
|
+
disabled={disabled}
|
|
13
|
+
Icon={getIcon(variant, value, isSelected)}
|
|
14
|
+
onPress={() => onChange(value)}
|
|
15
|
+
style={{ root: wrapperStyle }}
|
|
16
|
+
variant={getVariant(variant, isSelected)}
|
|
17
|
+
>
|
|
18
|
+
{RATING_VARIANTS.TEXT === variant ? name : null}
|
|
19
|
+
</UTButton>
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
Option.propTypes = {
|
|
23
|
+
disabled: bool,
|
|
24
|
+
isSelected: bool,
|
|
25
|
+
name: string,
|
|
26
|
+
onChange: func,
|
|
27
|
+
value: string,
|
|
28
|
+
variant: string,
|
|
29
|
+
wrapperStyle: object
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export default Option;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { RATING_VARIANTS } from '../../constants';
|
|
2
|
+
|
|
3
|
+
export const getIcon = (variant, value, isSelected) =>
|
|
4
|
+
({
|
|
5
|
+
[RATING_VARIANTS.TEXT]: null,
|
|
6
|
+
[RATING_VARIANTS.FACES]: {
|
|
7
|
+
1: 'IconMoodSad',
|
|
8
|
+
2: 'IconMoodConfuzed',
|
|
9
|
+
3: 'IconMoodEmpty',
|
|
10
|
+
4: 'IconMoodSmile',
|
|
11
|
+
5: 'IconMoodHappy'
|
|
12
|
+
}[value],
|
|
13
|
+
[RATING_VARIANTS.STAR]: isSelected ? 'IconStarFilled' : 'IconStar'
|
|
14
|
+
})[variant];
|
|
15
|
+
|
|
16
|
+
export const getVariant = (variant, isSelected) =>
|
|
17
|
+
!isSelected || variant === RATING_VARIANTS.STAR ? 'shadow' : 'outlined';
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { func, string, bool, object, array } from 'prop-types';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import { formatErrorToValidation } from '../UTValidation/utils';
|
|
6
|
+
import { withTheme } from '../../theming';
|
|
7
|
+
import UTFieldLabel from '../UTFieldLabel';
|
|
8
|
+
import UTLabel from '../UTLabel';
|
|
9
|
+
import UTValidation from '../UTValidation';
|
|
10
|
+
|
|
11
|
+
import { RATING_VARIANTS } from './constants';
|
|
12
|
+
import Option from './components/Option';
|
|
13
|
+
import styles from './styles';
|
|
14
|
+
|
|
15
|
+
const UTRating = ({
|
|
16
|
+
classNames = {},
|
|
17
|
+
dataTestId,
|
|
18
|
+
disabled,
|
|
19
|
+
error,
|
|
20
|
+
helpTextEnd,
|
|
21
|
+
helpTextStart,
|
|
22
|
+
onChange,
|
|
23
|
+
options = [],
|
|
24
|
+
required,
|
|
25
|
+
theme,
|
|
26
|
+
title,
|
|
27
|
+
validations,
|
|
28
|
+
value,
|
|
29
|
+
variant = RATING_VARIANTS.STAR
|
|
30
|
+
}) => {
|
|
31
|
+
const validationData = useMemo(
|
|
32
|
+
() => validations || (error && formatErrorToValidation(error)),
|
|
33
|
+
[error, validations]
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const valueIndex = options.map(({ value: optionValue }) => optionValue).indexOf(value);
|
|
37
|
+
const isSelected = index =>
|
|
38
|
+
valueIndex !== -1 && variant === RATING_VARIANTS.STAR ? index <= valueIndex : index === valueIndex;
|
|
39
|
+
|
|
40
|
+
const [containerWidth, setContainerWidth] = useState(0);
|
|
41
|
+
|
|
42
|
+
const onLayout = useCallback(event => {
|
|
43
|
+
const { width } = event.nativeEvent.layout;
|
|
44
|
+
setContainerWidth(width);
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<View style={[styles.container, classNames.container]} data-testid={dataTestId}>
|
|
49
|
+
{title && (
|
|
50
|
+
<UTFieldLabel colorTheme="dark" required={required}>
|
|
51
|
+
{title}
|
|
52
|
+
</UTFieldLabel>
|
|
53
|
+
)}
|
|
54
|
+
<View style={styles.optionsContainer} onLayout={onLayout}>
|
|
55
|
+
{options.map((option, index) => (
|
|
56
|
+
<Option
|
|
57
|
+
{...option}
|
|
58
|
+
disabled={disabled}
|
|
59
|
+
error={error}
|
|
60
|
+
isSelected={isSelected(index)}
|
|
61
|
+
key={option.value}
|
|
62
|
+
onChange={onChange}
|
|
63
|
+
variant={variant}
|
|
64
|
+
wrapperStyle={styles.option(containerWidth, error, options, theme)}
|
|
65
|
+
/>
|
|
66
|
+
))}
|
|
67
|
+
</View>
|
|
68
|
+
{helpTextStart && (
|
|
69
|
+
<View style={styles.helpTextContainer}>
|
|
70
|
+
<UTLabel colorTheme="gray" variant="small">
|
|
71
|
+
{helpTextStart}
|
|
72
|
+
</UTLabel>
|
|
73
|
+
{helpTextEnd && (
|
|
74
|
+
<UTLabel colorTheme="gray" variant="small">
|
|
75
|
+
{helpTextEnd}
|
|
76
|
+
</UTLabel>
|
|
77
|
+
)}
|
|
78
|
+
</View>
|
|
79
|
+
)}
|
|
80
|
+
{validationData && <UTValidation validationData={validationData} />}
|
|
81
|
+
</View>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
UTRating.propTypes = {
|
|
86
|
+
classNames: object,
|
|
87
|
+
dataTestId: string,
|
|
88
|
+
disabled: bool,
|
|
89
|
+
error: string,
|
|
90
|
+
helpTextEnd: string,
|
|
91
|
+
helpTextStart: string,
|
|
92
|
+
onChange: func,
|
|
93
|
+
options: array,
|
|
94
|
+
required: bool,
|
|
95
|
+
title: string,
|
|
96
|
+
validations: array,
|
|
97
|
+
value: string,
|
|
98
|
+
variant: string
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export default withTheme(UTRating);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export default StyleSheet.create({
|
|
4
|
+
container: {
|
|
5
|
+
display: 'flex',
|
|
6
|
+
flexDirection: 'column',
|
|
7
|
+
gap: 8,
|
|
8
|
+
width: '100%'
|
|
9
|
+
},
|
|
10
|
+
optionsContainer: {
|
|
11
|
+
display: 'flex',
|
|
12
|
+
flexDirection: 'row',
|
|
13
|
+
flexWrap: 'wrap',
|
|
14
|
+
gap: 4,
|
|
15
|
+
justifyContent: 'center',
|
|
16
|
+
width: '100%'
|
|
17
|
+
},
|
|
18
|
+
helpTextContainer: {
|
|
19
|
+
display: 'flex',
|
|
20
|
+
flexDirection: 'row',
|
|
21
|
+
justifyContent: 'space-between'
|
|
22
|
+
},
|
|
23
|
+
option: (containerWidth, error, options, theme) => {
|
|
24
|
+
const topItems = Math.ceil(options.length / 2);
|
|
25
|
+
const breakOptions = options.length > 5;
|
|
26
|
+
return {
|
|
27
|
+
alignContent: 'center',
|
|
28
|
+
display: 'flex',
|
|
29
|
+
flexBasis: breakOptions ? (containerWidth - (topItems - 1) * 4) / topItems : 0,
|
|
30
|
+
flexDirection: 'row',
|
|
31
|
+
flexGrow: breakOptions ? 0 : 1,
|
|
32
|
+
flexShrink: 0,
|
|
33
|
+
height: 48,
|
|
34
|
+
justifyContent: 'center',
|
|
35
|
+
...(error ? { borderColor: theme.Palette.error['05'], borderWidth: 2 } : {})
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { bool, number, string } from 'prop-types';
|
|
2
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { Animated, View } from 'react-native';
|
|
4
|
+
|
|
5
|
+
import { useTheme } from '../../../../../../theming';
|
|
6
|
+
import { mergeMultipleStyles } from '../../../../../../utils/styleUtils';
|
|
7
|
+
import { BAR_HEIGHT_DELAY, BAR_HEIGHT_DURATION } from '../../constants';
|
|
8
|
+
|
|
9
|
+
import styles, { getVariantStyles } from './styles';
|
|
10
|
+
|
|
11
|
+
const BarMask = ({ stepCompleted, height, variant }) => {
|
|
12
|
+
const theme = useTheme();
|
|
13
|
+
const ownStyles = mergeMultipleStyles(styles, getVariantStyles(theme)[variant]);
|
|
14
|
+
|
|
15
|
+
const heightAnim = useRef(new Animated.Value(0)).current;
|
|
16
|
+
const [inverted, setInverted] = useState(false);
|
|
17
|
+
|
|
18
|
+
const animateCompleted = () => {
|
|
19
|
+
Animated.sequence([
|
|
20
|
+
Animated.delay(BAR_HEIGHT_DELAY),
|
|
21
|
+
Animated.timing(heightAnim, {
|
|
22
|
+
toValue: height,
|
|
23
|
+
duration: BAR_HEIGHT_DURATION / 2,
|
|
24
|
+
useNativeDriver: false
|
|
25
|
+
}),
|
|
26
|
+
Animated.timing(heightAnim, {
|
|
27
|
+
toValue: 0,
|
|
28
|
+
duration: BAR_HEIGHT_DURATION / 2,
|
|
29
|
+
useNativeDriver: false
|
|
30
|
+
})
|
|
31
|
+
]).start();
|
|
32
|
+
setTimeout(() => setInverted(true), BAR_HEIGHT_DELAY + BAR_HEIGHT_DURATION / 2);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (stepCompleted) animateCompleted();
|
|
37
|
+
else setInverted(false);
|
|
38
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
39
|
+
}, [stepCompleted]);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<View style={ownStyles.container(height)}>
|
|
43
|
+
<Animated.View style={[ownStyles.barMask, inverted && ownStyles.inverted, { height: heightAnim }]} />
|
|
44
|
+
</View>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
BarMask.propTypes = {
|
|
49
|
+
stepCompleted: bool,
|
|
50
|
+
height: number,
|
|
51
|
+
variant: string
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export default BarMask;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { OVAL_SIZE } from '../../../../styles';
|
|
4
|
+
import { ERROR, STANDARD } from '../../../../constants';
|
|
5
|
+
|
|
6
|
+
export default StyleSheet.create({
|
|
7
|
+
container: height => ({
|
|
8
|
+
height,
|
|
9
|
+
width: OVAL_SIZE,
|
|
10
|
+
top: 20,
|
|
11
|
+
position: 'absolute',
|
|
12
|
+
flex: 1,
|
|
13
|
+
justifyContent: 'center',
|
|
14
|
+
flexDirection: 'row',
|
|
15
|
+
zIndex: 10
|
|
16
|
+
}),
|
|
17
|
+
barMask: {
|
|
18
|
+
width: 2,
|
|
19
|
+
alignSelf: 'flex-start'
|
|
20
|
+
},
|
|
21
|
+
inverted: { alignSelf: 'flex-end' }
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const getVariantStyles = theme => ({
|
|
25
|
+
[ERROR]: {
|
|
26
|
+
barMask: { backgroundColor: theme.Palette.error['04'] }
|
|
27
|
+
},
|
|
28
|
+
[STANDARD]: {
|
|
29
|
+
barMask: { backgroundColor: theme.Palette.accent['04'] }
|
|
30
|
+
}
|
|
31
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import { View, Animated } from 'react-native';
|
|
3
|
+
import MaskedView from '@react-native-masked-view/masked-view';
|
|
4
|
+
import { bool, string } from 'prop-types';
|
|
5
|
+
|
|
6
|
+
import { mergeMultipleStyles } from '../../../../../../utils/styleUtils';
|
|
7
|
+
import { useTheme } from '../../../../../../theming';
|
|
8
|
+
import {
|
|
9
|
+
ACTIVE_STEP_HEIGHT_DELAY,
|
|
10
|
+
ACTIVE_STEP_HEIGHT_DURATION,
|
|
11
|
+
COMPLETED_STEP_HEIGHT_DELAY,
|
|
12
|
+
COMPLETED_STEP_HEIGHT_DURATION
|
|
13
|
+
} from '../../constants';
|
|
14
|
+
|
|
15
|
+
import styles, { getVariantStyles, MASK_SIZE } from './styles';
|
|
16
|
+
|
|
17
|
+
const StepMask = ({ stepCompleted, stepActive, variant }) => {
|
|
18
|
+
const theme = useTheme();
|
|
19
|
+
const ownStyles = mergeMultipleStyles(styles, getVariantStyles(theme)[variant]);
|
|
20
|
+
|
|
21
|
+
const heightAnim = useRef(new Animated.Value(MASK_SIZE)).current;
|
|
22
|
+
|
|
23
|
+
const handleCompleted = () => {
|
|
24
|
+
heightAnim.setValue(MASK_SIZE);
|
|
25
|
+
Animated.sequence([
|
|
26
|
+
Animated.delay(COMPLETED_STEP_HEIGHT_DELAY),
|
|
27
|
+
Animated.timing(heightAnim, {
|
|
28
|
+
toValue: 0,
|
|
29
|
+
duration: COMPLETED_STEP_HEIGHT_DURATION,
|
|
30
|
+
useNativeDriver: false
|
|
31
|
+
})
|
|
32
|
+
]).start();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const handleActive = () => {
|
|
36
|
+
heightAnim.setValue(0);
|
|
37
|
+
Animated.sequence([
|
|
38
|
+
Animated.delay(ACTIVE_STEP_HEIGHT_DELAY),
|
|
39
|
+
Animated.timing(heightAnim, {
|
|
40
|
+
toValue: MASK_SIZE,
|
|
41
|
+
duration: ACTIVE_STEP_HEIGHT_DURATION,
|
|
42
|
+
useNativeDriver: false
|
|
43
|
+
})
|
|
44
|
+
]).start();
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (stepCompleted) handleCompleted();
|
|
49
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
50
|
+
}, [stepCompleted]);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (stepActive) handleActive();
|
|
54
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
55
|
+
}, [stepActive]);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<View style={ownStyles.container}>
|
|
59
|
+
<MaskedView
|
|
60
|
+
style={ownStyles.maskContainer}
|
|
61
|
+
maskElement={
|
|
62
|
+
<View style={ownStyles.mask}>
|
|
63
|
+
<View style={ownStyles.maskElement} />
|
|
64
|
+
</View>
|
|
65
|
+
}
|
|
66
|
+
>
|
|
67
|
+
{/* Shows behind the mask */}
|
|
68
|
+
{(stepCompleted || stepActive) && (
|
|
69
|
+
<Animated.View
|
|
70
|
+
style={[ownStyles.maskBackground, stepActive && ownStyles.maskActive, { height: heightAnim }]}
|
|
71
|
+
/>
|
|
72
|
+
)}
|
|
73
|
+
</MaskedView>
|
|
74
|
+
</View>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
StepMask.propTypes = {
|
|
79
|
+
stepCompleted: bool,
|
|
80
|
+
stepActive: bool,
|
|
81
|
+
variant: string
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export default StepMask;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { ERROR, STANDARD } from '../../../../constants';
|
|
4
|
+
|
|
5
|
+
export const MASK_SIZE = 20;
|
|
6
|
+
|
|
7
|
+
export default StyleSheet.create({
|
|
8
|
+
container: {
|
|
9
|
+
height: MASK_SIZE,
|
|
10
|
+
width: MASK_SIZE,
|
|
11
|
+
position: 'absolute',
|
|
12
|
+
zIndex: 1
|
|
13
|
+
},
|
|
14
|
+
mask: {
|
|
15
|
+
// Transparent background because mask is based off alpha channel.
|
|
16
|
+
backgroundColor: 'transparent',
|
|
17
|
+
flex: 1
|
|
18
|
+
},
|
|
19
|
+
maskElement: {
|
|
20
|
+
height: MASK_SIZE,
|
|
21
|
+
width: MASK_SIZE,
|
|
22
|
+
borderColor: 'black',
|
|
23
|
+
borderWidth: 2,
|
|
24
|
+
borderRadius: MASK_SIZE / 2
|
|
25
|
+
},
|
|
26
|
+
maskBackground: { flex: 1, alignSelf: 'flex-end' },
|
|
27
|
+
maskContainer: { flex: 1, flexDirection: 'row', height: '100%' },
|
|
28
|
+
maskActive: { alignSelf: 'flex-start' }
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const getVariantStyles = theme => ({
|
|
32
|
+
[ERROR]: {
|
|
33
|
+
maskBackground: { backgroundColor: theme.Palette.error['04'] }
|
|
34
|
+
},
|
|
35
|
+
[STANDARD]: {
|
|
36
|
+
maskBackground: { backgroundColor: theme.Palette.accent['04'] }
|
|
37
|
+
}
|
|
38
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const COMPLETED_STEP_OPACITY_DURATION = 300;
|
|
2
|
+
export const COMPLETED_STEP_HEIGHT_DELAY = 800;
|
|
3
|
+
export const COMPLETED_STEP_HEIGHT_DURATION = 300;
|
|
4
|
+
|
|
5
|
+
export const ACTIVE_STEP_DELAY = 1800;
|
|
6
|
+
export const ACTIVE_STEP_HEIGHT_DELAY = 1100;
|
|
7
|
+
export const ACTIVE_STEP_HEIGHT_DURATION = 300;
|
|
8
|
+
|
|
9
|
+
export const BAR_HEIGHT_DELAY = 800;
|
|
10
|
+
export const BAR_HEIGHT_DURATION = 600;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import React, { useEffect, useState } from 'react';
|
|
2
|
-
import { View } from 'react-native';
|
|
3
|
-
import { bool, func, number } from 'prop-types';
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { View, Animated } from 'react-native';
|
|
3
|
+
import { array, bool, func, number } from 'prop-types';
|
|
4
4
|
import merge from 'lodash/merge';
|
|
5
5
|
|
|
6
6
|
import { StepPropTypes, VariantPropTypes } from '../../propTypes';
|
|
@@ -8,12 +8,26 @@ import UTIcon from '../../../UTIcon';
|
|
|
8
8
|
import UTLabel from '../../../UTLabel';
|
|
9
9
|
import { ERROR } from '../../constants';
|
|
10
10
|
import { useTheme } from '../../../../theming';
|
|
11
|
+
import { OVAL_SIZE } from '../../styles';
|
|
11
12
|
|
|
12
13
|
import { getVariantStyles, getStepStyles } from './styles';
|
|
14
|
+
import StepMask from './components/StepMask';
|
|
15
|
+
import BarMask from './components/BarMask';
|
|
16
|
+
import { ACTIVE_STEP_DELAY, COMPLETED_STEP_OPACITY_DURATION } from './constants';
|
|
13
17
|
|
|
14
18
|
const ERROR_ICON_SIZE = 16;
|
|
15
19
|
|
|
16
|
-
const Step = ({
|
|
20
|
+
const Step = ({
|
|
21
|
+
first,
|
|
22
|
+
index,
|
|
23
|
+
isActive,
|
|
24
|
+
isCompleted,
|
|
25
|
+
stepsPositions,
|
|
26
|
+
setStepsPositions,
|
|
27
|
+
step,
|
|
28
|
+
style = {},
|
|
29
|
+
variant
|
|
30
|
+
}) => {
|
|
17
31
|
const stepCompleted = isCompleted(step.id);
|
|
18
32
|
const stepActive = isActive(step.id);
|
|
19
33
|
|
|
@@ -23,6 +37,25 @@ const Step = ({ first, index, isActive, isCompleted, setStepsPositions, step, st
|
|
|
23
37
|
const [stepIconOffset, setStepIconOffset] = useState(0);
|
|
24
38
|
const [currentPosition, setCurrentPosition] = useState(0);
|
|
25
39
|
|
|
40
|
+
const opacityAnim = useRef(new Animated.Value(1)).current;
|
|
41
|
+
const animateCompleted = () => {
|
|
42
|
+
Animated.timing(opacityAnim, {
|
|
43
|
+
toValue: 0.5,
|
|
44
|
+
duration: COMPLETED_STEP_OPACITY_DURATION,
|
|
45
|
+
useNativeDriver: true
|
|
46
|
+
}).start();
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const [delayedStepActive, setDelayedStepActive] = useState(false);
|
|
50
|
+
// eslint-disable-next-line consistent-return
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (stepActive) {
|
|
53
|
+
const timer = setTimeout(() => setDelayedStepActive(true), index === 0 ? 0 : ACTIVE_STEP_DELAY);
|
|
54
|
+
return () => clearTimeout(timer);
|
|
55
|
+
}
|
|
56
|
+
setDelayedStepActive(false);
|
|
57
|
+
}, [index, stepActive]);
|
|
58
|
+
|
|
26
59
|
useEffect(() => {
|
|
27
60
|
setStepsPositions(prev => {
|
|
28
61
|
const newPrev = [...prev];
|
|
@@ -31,43 +64,54 @@ const Step = ({ first, index, isActive, isCompleted, setStepsPositions, step, st
|
|
|
31
64
|
});
|
|
32
65
|
}, [stepIconOffset, currentPosition, setCurrentPosition, index, setStepsPositions]);
|
|
33
66
|
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (stepCompleted) animateCompleted();
|
|
69
|
+
else opacityAnim.setValue(1);
|
|
70
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
71
|
+
}, [stepCompleted]);
|
|
72
|
+
|
|
73
|
+
const barHeight = Math.max((stepsPositions[index + 1] ?? 0) - (stepsPositions[index] ?? 0) - OVAL_SIZE, 24);
|
|
74
|
+
|
|
34
75
|
return (
|
|
35
76
|
<View
|
|
36
77
|
onLayout={e => setCurrentPosition(e.nativeEvent.layout.y)}
|
|
37
78
|
style={[
|
|
38
79
|
themedStyles.outerContainer,
|
|
39
80
|
stepCompleted && themedStyles.completedOuterContainer,
|
|
40
|
-
|
|
81
|
+
delayedStepActive && themedStyles.activeOuterContainer,
|
|
41
82
|
!first && ownStyles.stepMargin
|
|
42
83
|
]}
|
|
43
84
|
>
|
|
44
|
-
<
|
|
85
|
+
<StepMask stepCompleted={stepCompleted} stepActive={stepActive} variant={variant} />
|
|
86
|
+
<BarMask stepCompleted={stepCompleted} height={barHeight} variant={variant} />
|
|
87
|
+
<Animated.View
|
|
45
88
|
onLayout={e => setStepIconOffset(e.nativeEvent.layout.y)}
|
|
46
89
|
style={[
|
|
47
90
|
themedStyles.container,
|
|
48
91
|
stepCompleted && themedStyles.completedContainer,
|
|
49
|
-
|
|
92
|
+
{ opacity: opacityAnim },
|
|
93
|
+
delayedStepActive && themedStyles.activeContainer
|
|
50
94
|
]}
|
|
51
95
|
>
|
|
52
96
|
<View
|
|
53
97
|
style={[
|
|
54
98
|
themedStyles.innerContainer,
|
|
55
99
|
stepCompleted && themedStyles.completedInnerContainer,
|
|
56
|
-
|
|
100
|
+
delayedStepActive && themedStyles.activeInnerContainer
|
|
57
101
|
]}
|
|
58
102
|
>
|
|
59
|
-
{
|
|
103
|
+
{delayedStepActive && variant === ERROR && (
|
|
60
104
|
<UTIcon name="IconX" colorTheme="negative" size={ERROR_ICON_SIZE} style={themedStyles.icon} />
|
|
61
105
|
)}
|
|
62
106
|
</View>
|
|
63
|
-
</View>
|
|
64
|
-
<View style={themedStyles.textContainer}>
|
|
107
|
+
</Animated.View>
|
|
108
|
+
<Animated.View style={[themedStyles.textContainer, { opacity: opacityAnim }]}>
|
|
65
109
|
{step.title && (
|
|
66
110
|
<UTLabel
|
|
67
111
|
variant="small"
|
|
68
112
|
weight="medium"
|
|
69
113
|
colorTheme={
|
|
70
|
-
stepCompleted ? 'gray' :
|
|
114
|
+
stepCompleted ? 'gray' : delayedStepActive ? (variant === ERROR ? 'error' : 'accent') : 'dark'
|
|
71
115
|
}
|
|
72
116
|
>
|
|
73
117
|
{step.title}
|
|
@@ -80,7 +124,7 @@ const Step = ({ first, index, isActive, isCompleted, setStepsPositions, step, st
|
|
|
80
124
|
</UTLabel>
|
|
81
125
|
</View>
|
|
82
126
|
)}
|
|
83
|
-
</View>
|
|
127
|
+
</Animated.View>
|
|
84
128
|
</View>
|
|
85
129
|
);
|
|
86
130
|
};
|
|
@@ -91,6 +135,7 @@ Step.propTypes = {
|
|
|
91
135
|
isActive: func,
|
|
92
136
|
isCompleted: func,
|
|
93
137
|
setStepsPositions: func,
|
|
138
|
+
stepsPositions: array,
|
|
94
139
|
step: StepPropTypes,
|
|
95
140
|
variant: VariantPropTypes
|
|
96
141
|
};
|
|
@@ -17,9 +17,6 @@ export const getVariantStyles = theme => ({
|
|
|
17
17
|
},
|
|
18
18
|
completedInnerContainer: {
|
|
19
19
|
backgroundColor: theme.Palette.error['04']
|
|
20
|
-
},
|
|
21
|
-
completedOuterContainer: {
|
|
22
|
-
opacity: 0.5
|
|
23
20
|
}
|
|
24
21
|
},
|
|
25
22
|
[STANDARD]: {
|
|
@@ -34,9 +31,6 @@ export const getVariantStyles = theme => ({
|
|
|
34
31
|
},
|
|
35
32
|
completedInnerContainer: {
|
|
36
33
|
backgroundColor: theme.Palette.accent['04']
|
|
37
|
-
},
|
|
38
|
-
completedOuterContainer: {
|
|
39
|
-
opacity: 0.5
|
|
40
34
|
}
|
|
41
35
|
}
|
|
42
36
|
});
|
package/lib/index.js
CHANGED
|
@@ -57,6 +57,7 @@ export { default as UTPasswordField } from './components/UTPasswordField';
|
|
|
57
57
|
export { default as UTPhoneInput } from './components/UTPhoneInput';
|
|
58
58
|
export { default as UTProductItem } from './components/UTProductItem';
|
|
59
59
|
export { default as UTProgressBar } from './components/UTProgressBar';
|
|
60
|
+
export { default as UTRating } from './components/UTRating';
|
|
60
61
|
export { default as UTRoundView } from './components/UTRoundView';
|
|
61
62
|
export { default as UTSearchField } from './components/UTSearchField';
|
|
62
63
|
export { default as UTSelect } from './components/UTSelect';
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@widergy/mobile-ui",
|
|
3
3
|
"description": "Widergy Mobile Components",
|
|
4
4
|
"author": "widergy",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.32.0",
|
|
6
6
|
"repository": "https://github.com/widergy/mobile-ui.git",
|
|
7
7
|
"main": "lib/index.js",
|
|
8
8
|
"files": [
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"react-native-vector-icons": "^10.0.0"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
+
"@react-native-masked-view/masked-view": "^0.3.1",
|
|
38
39
|
"@react-navigation/native": "^6.1.9",
|
|
39
40
|
"@tabler/icons-react-native": "^3.3.0",
|
|
40
41
|
"@widergy/web-utils": "^2.0.0",
|