@utilitywarehouse/hearth-react-native 0.23.0 → 0.24.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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-lint.log +13 -13
- package/CHANGELOG.md +36 -0
- package/build/components/Modal/Modal.js +5 -4
- package/build/components/Modal/Modal.props.d.ts +10 -4
- package/build/components/ProgressBar/ProgressBar.d.ts +6 -0
- package/build/components/ProgressBar/ProgressBar.js +35 -0
- package/build/components/ProgressBar/ProgressBar.props.d.ts +60 -0
- package/build/components/ProgressBar/ProgressBar.props.js +1 -0
- package/build/components/ProgressBar/ProgressBarCircular.d.ts +6 -0
- package/build/components/ProgressBar/ProgressBarCircular.js +115 -0
- package/build/components/ProgressBar/ProgressBarLinear.d.ts +6 -0
- package/build/components/ProgressBar/ProgressBarLinear.js +79 -0
- package/build/components/ProgressBar/index.d.ts +2 -0
- package/build/components/ProgressBar/index.js +1 -0
- package/build/components/index.d.ts +1 -0
- package/build/components/index.js +1 -0
- package/docs/components/AllComponents.web.tsx +6 -0
- package/package.json +1 -1
- package/src/components/Modal/Modal.props.ts +13 -4
- package/src/components/Modal/Modal.stories.tsx +1 -1
- package/src/components/Modal/Modal.tsx +28 -11
- package/src/components/ProgressBar/ProgressBar.docs.mdx +90 -0
- package/src/components/ProgressBar/ProgressBar.figma.tsx +79 -0
- package/src/components/ProgressBar/ProgressBar.props.ts +60 -0
- package/src/components/ProgressBar/ProgressBar.stories.tsx +117 -0
- package/src/components/ProgressBar/ProgressBar.tsx +74 -0
- package/src/components/ProgressBar/ProgressBarCircular.tsx +181 -0
- package/src/components/ProgressBar/ProgressBarLinear.tsx +127 -0
- package/src/components/ProgressBar/index.ts +7 -0
- package/src/components/index.ts +1 -0
package/.turbo/turbo-build.log
CHANGED
package/.turbo/turbo-lint.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @utilitywarehouse/hearth-react-native@0.
|
|
2
|
+
> @utilitywarehouse/hearth-react-native@0.24.0 lint /home/runner/work/hearth/hearth/packages/react-native
|
|
3
3
|
> TIMING=1 eslint .
|
|
4
4
|
|
|
5
5
|
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
78:8 warning React Hook useEffect has a missing dependency: 'formFieldContext'. Either include it or remove the dependency array react-hooks/exhaustive-deps
|
|
32
32
|
|
|
33
33
|
/home/runner/work/hearth/hearth/packages/react-native/src/components/Modal/Modal.tsx
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
75:6 warning React Hook useCallback has an unnecessary dependency: 'Platform.OS'. Either exclude it or remove the dependency array. Outer scope values like 'Platform.OS' aren't valid dependencies because mutating them doesn't re-render the component react-hooks/exhaustive-deps
|
|
35
|
+
291:5 warning React Hook useCallback has a missing dependency: 'footer'. Either include it or remove the dependency array react-hooks/exhaustive-deps
|
|
36
36
|
|
|
37
37
|
/home/runner/work/hearth/hearth/packages/react-native/src/components/Modal/Modal.web.tsx
|
|
38
38
|
66:6 warning React Hook useCallback has an unnecessary dependency: 'Platform.OS'. Either exclude it or remove the dependency array. Outer scope values like 'Platform.OS' aren't valid dependencies because mutating them doesn't re-render the component react-hooks/exhaustive-deps
|
|
@@ -60,13 +60,13 @@
|
|
|
60
60
|
|
|
61
61
|
Rule | Time (ms) | Relative
|
|
62
62
|
:-----------------------------------------|----------:|--------:
|
|
63
|
-
@typescript-eslint/no-unused-vars |
|
|
64
|
-
react-hooks/exhaustive-deps |
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
@typescript-eslint/ban-ts-comment |
|
|
68
|
-
no-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
no-
|
|
72
|
-
no-
|
|
63
|
+
@typescript-eslint/no-unused-vars | 1400.036 | 60.2%
|
|
64
|
+
react-hooks/exhaustive-deps | 100.065 | 4.3%
|
|
65
|
+
react-hooks/rules-of-hooks | 83.204 | 3.6%
|
|
66
|
+
no-global-assign | 71.807 | 3.1%
|
|
67
|
+
@typescript-eslint/ban-ts-comment | 53.514 | 2.3%
|
|
68
|
+
no-loss-of-precision | 35.722 | 1.5%
|
|
69
|
+
@typescript-eslint/triple-slash-reference | 35.355 | 1.5%
|
|
70
|
+
no-unexpected-multiline | 32.651 | 1.4%
|
|
71
|
+
no-useless-escape | 30.776 | 1.3%
|
|
72
|
+
no-misleading-character-class | 29.528 | 1.3%
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# @utilitywarehouse/hearth-react-native
|
|
2
2
|
|
|
3
|
+
## 0.24.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#977](https://github.com/utilitywarehouse/hearth/pull/977) [`9d2b534`](https://github.com/utilitywarehouse/hearth/commit/9d2b5348a5748cb613f537808069de2e86bd21d7) Thanks [@jordmccord](https://github.com/jordmccord)! - 🌟 [FEATURE]: Add `ProgressBar` component with linear and circular variants.
|
|
8
|
+
|
|
9
|
+
**Developer changes**:
|
|
10
|
+
|
|
11
|
+
Use `ProgressBar` with a default percentage label, or override the label to show a custom value:
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { ProgressBar } from '@utilitywarehouse/hearth-react-native';
|
|
15
|
+
|
|
16
|
+
<ProgressBar value={42} label="Uploading documents" />
|
|
17
|
+
|
|
18
|
+
<ProgressBar
|
|
19
|
+
value={68}
|
|
20
|
+
max={100}
|
|
21
|
+
label="Data allowance"
|
|
22
|
+
variant="circular"
|
|
23
|
+
formatValueText={(value, { max }) => `${max - value}GB remaining`}
|
|
24
|
+
/>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- [#978](https://github.com/utilitywarehouse/hearth/pull/978) [`26a1173`](https://github.com/utilitywarehouse/hearth/commit/26a11731a493a8b92ac2a3a183516376ab54663b) Thanks [@jordmccord](https://github.com/jordmccord)! - 💅 [ENHANCEMENT]: Tighten `Modal` prop types and fix brand background text styling
|
|
30
|
+
|
|
31
|
+
Improves TypeScript safety so `stickyFooter` is not allowed when `inNavModal` is true, and `background` can only be set when `inNavModal` is true. Also ensures headings, body text, and button content are correctly inverted when using the brand background.
|
|
32
|
+
|
|
33
|
+
**Components affected**:
|
|
34
|
+
- `Modal`
|
|
35
|
+
|
|
36
|
+
**Developer changes**:
|
|
37
|
+
No changes required unless you were relying on invalid prop combinations.
|
|
38
|
+
|
|
3
39
|
## 0.23.0
|
|
4
40
|
|
|
5
41
|
### Minor Changes
|
|
@@ -20,6 +20,7 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
|
|
|
20
20
|
const theme = useTheme();
|
|
21
21
|
const backgroundOpacity = useSharedValue(0);
|
|
22
22
|
const pretendContentTranslateY = useSharedValue(20);
|
|
23
|
+
const isBrandBackground = background === 'brand';
|
|
23
24
|
const triggerCloseAnimation = useCallback(() => {
|
|
24
25
|
if (Platform.OS === 'android' && inNavModal) {
|
|
25
26
|
pretendContentTranslateY.value = withTiming(20, {
|
|
@@ -105,10 +106,10 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
|
|
|
105
106
|
noButtons,
|
|
106
107
|
stickyFooter,
|
|
107
108
|
showHandle: props.showHandle,
|
|
108
|
-
background:
|
|
109
|
+
background: isBrandBackground ? 'brand' : 'primary',
|
|
109
110
|
});
|
|
110
|
-
const footer = (_jsxs(View, { style: styles.footer, children: [onPressPrimaryButton && primaryButtonText ? (_jsx(Button, { onPress: handlePrimaryButtonPress, text: primaryButtonText, inverted:
|
|
111
|
-
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" }), _jsx(Heading, { size: "lg", textAlign: "center", 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, children: heading })) : null, description && !image ? _jsx(BodyText, { accessible: true, children: description }) : null] }), showCloseButton ? (_jsx(UnstyledIconButton, { icon: CloseMediumIcon, onPress: handleCloseButtonPress, accessibilityLabel: "Close modal", inverted:
|
|
111
|
+
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] }));
|
|
112
|
+
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 ? _jsx(ScrollView, { style: { flex: 1 }, children: children }) : children, (!stickyFooter || inNavModal) && !noButtons ? footer : null] })) }));
|
|
112
113
|
const renderFooter = useCallback((props) => (_jsx(BottomSheetFooter, { ...props, children: _jsx(View, { style: styles.footerWrap, children: footer }) })), [
|
|
113
114
|
onPressPrimaryButton,
|
|
114
115
|
primaryButtonText,
|
|
@@ -119,7 +120,7 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
|
|
|
119
120
|
]);
|
|
120
121
|
return inNavModal ? (_jsxs(View, { style: {
|
|
121
122
|
flex: 1,
|
|
122
|
-
backgroundColor: theme.color.background[
|
|
123
|
+
backgroundColor: theme.color.background[isBrandBackground ? 'brand' : 'primary'],
|
|
123
124
|
}, children: [Platform.OS === 'android' ? (_jsx(Animated.View, { style: [styles.androidContainer, animatedBackgroundStyle], children: _jsx(Animated.View, { style: [styles.pretendContent, animatedPretendContentStyle] }) })) : null, _jsx(Animated.View, { style: [styles.inNavModalContainer, Platform.OS === 'android' && animatedInNavModalStyle], children: _jsx(View, { style: styles.inNavModalContent, children: content }) })] })) : (_jsxs(BottomSheetModal, { ref: bottomSheetModalRef, enableDynamicSizing: true, snapPoints: image || fullscreen ? ['90%'] : props.snapPoints, showHandle: typeof loading !== 'undefined' && loading ? false : props.showHandle, accessible: false, style: styles.modal, footerComponent: stickyFooter && !noButtons ? renderFooter : undefined, ...props, onChange: handleChange, children: [loading ? _jsx(View, { style: styles.loadingTop }) : null, _jsx(BottomSheetScrollView, { contentContainerStyle: styles.scrollView, ref: scrollViewRef, children: content })] }));
|
|
124
125
|
};
|
|
125
126
|
const styles = StyleSheet.create((theme, rt) => ({
|
|
@@ -3,16 +3,14 @@ import { ViewProps } from 'react-native';
|
|
|
3
3
|
import { BottomSheetProps } from '../BottomSheet';
|
|
4
4
|
import { ButtonWithoutChildrenProps } from '../Button/Button.props';
|
|
5
5
|
import { UnstyledIconButtonProps } from '../UnstyledIconButton';
|
|
6
|
-
interface
|
|
6
|
+
interface ModalPropsBase extends Omit<BottomSheetProps, 'children'> {
|
|
7
7
|
loading?: boolean;
|
|
8
8
|
image?: ReactNode;
|
|
9
9
|
showCloseButton?: boolean;
|
|
10
10
|
heading?: string;
|
|
11
11
|
loadingHeading?: string;
|
|
12
12
|
description?: string;
|
|
13
|
-
inNavModal?: boolean;
|
|
14
13
|
fullscreen?: boolean;
|
|
15
|
-
stickyFooter?: boolean;
|
|
16
14
|
children?: ViewProps['children'];
|
|
17
15
|
onPressPrimaryButton?: () => void;
|
|
18
16
|
primaryButtonText?: string;
|
|
@@ -24,6 +22,14 @@ interface ModalProps extends Omit<BottomSheetProps, 'children'> {
|
|
|
24
22
|
primaryButtonProps?: Omit<ButtonWithoutChildrenProps, 'children'>;
|
|
25
23
|
secondaryButtonProps?: Omit<ButtonWithoutChildrenProps, 'children'>;
|
|
26
24
|
closeButtonProps?: Omit<UnstyledIconButtonProps, 'children'>;
|
|
27
|
-
background?: 'default' | 'brand';
|
|
28
25
|
}
|
|
26
|
+
type ModalProps = (ModalPropsBase & {
|
|
27
|
+
inNavModal?: false | undefined;
|
|
28
|
+
stickyFooter?: boolean;
|
|
29
|
+
background?: never;
|
|
30
|
+
}) | (ModalPropsBase & {
|
|
31
|
+
inNavModal: true;
|
|
32
|
+
stickyFooter?: never;
|
|
33
|
+
background?: 'default' | 'brand';
|
|
34
|
+
});
|
|
29
35
|
export default ModalProps;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type ProgressBarProps from './ProgressBar.props';
|
|
2
|
+
declare const ProgressBar: {
|
|
3
|
+
({ variant, colorScheme, size, value, min, max, label, hideLabel, formatValueText, "aria-valuetext": ariaValueText, accessibilityLabel, ...rest }: ProgressBarProps): import("react/jsx-runtime").JSX.Element;
|
|
4
|
+
displayName: string;
|
|
5
|
+
};
|
|
6
|
+
export default ProgressBar;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import ProgressBarCircular from './ProgressBarCircular';
|
|
4
|
+
import ProgressBarLinear from './ProgressBarLinear';
|
|
5
|
+
const clampValue = (value, min, max) => {
|
|
6
|
+
if (max <= min)
|
|
7
|
+
return min;
|
|
8
|
+
return Math.min(Math.max(value, min), max);
|
|
9
|
+
};
|
|
10
|
+
const valueToPercent = (value, min, max) => {
|
|
11
|
+
const range = max - min;
|
|
12
|
+
if (range <= 0)
|
|
13
|
+
return 0;
|
|
14
|
+
return ((value - min) / range) * 100;
|
|
15
|
+
};
|
|
16
|
+
const ProgressBar = ({ variant = 'linear', colorScheme = 'default', size = 'md', value, min = 0, max = 100, label, hideLabel, formatValueText, 'aria-valuetext': ariaValueText, accessibilityLabel, ...rest }) => {
|
|
17
|
+
const effectiveValue = colorScheme === 'success' && !formatValueText ? max : clampValue(value, min, max);
|
|
18
|
+
const percentValue = valueToPercent(effectiveValue, min, max);
|
|
19
|
+
const clampedPercent = Math.max(0, Math.min(100, percentValue));
|
|
20
|
+
const valueText = formatValueText
|
|
21
|
+
? formatValueText(effectiveValue, { min, max, percent: clampedPercent })
|
|
22
|
+
: `${Math.round(clampedPercent)}%`;
|
|
23
|
+
const valueTextForAria = ariaValueText ?? valueText;
|
|
24
|
+
const internalProps = {
|
|
25
|
+
percent: clampedPercent,
|
|
26
|
+
label,
|
|
27
|
+
valueText,
|
|
28
|
+
hideLabel,
|
|
29
|
+
colorScheme,
|
|
30
|
+
size,
|
|
31
|
+
};
|
|
32
|
+
return (_jsx(View, { ...rest, accessible: true, role: "progressbar", accessibilityRole: "progressbar", accessibilityLabel: accessibilityLabel ?? label, accessibilityValue: { min, max, now: effectiveValue, text: valueTextForAria }, "aria-valuenow": effectiveValue, "aria-valuemin": min, "aria-valuemax": max, "aria-valuetext": valueTextForAria, "data-colorscheme": colorScheme, children: variant === 'circular' ? (_jsx(ProgressBarCircular, { ...internalProps })) : (_jsx(ProgressBarLinear, { ...internalProps })) }));
|
|
33
|
+
};
|
|
34
|
+
ProgressBar.displayName = 'ProgressBar';
|
|
35
|
+
export default ProgressBar;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ViewProps } from 'react-native';
|
|
2
|
+
export type ProgressBarVariant = 'linear' | 'circular';
|
|
3
|
+
export type ProgressBarColorScheme = 'default' | 'success' | 'danger';
|
|
4
|
+
export type ProgressBarSize = 'sm' | 'md';
|
|
5
|
+
export interface ProgressBarProps extends ViewProps {
|
|
6
|
+
variant?: ProgressBarVariant;
|
|
7
|
+
/**
|
|
8
|
+
* Set the visual appearance.
|
|
9
|
+
* @default 'default'
|
|
10
|
+
*/
|
|
11
|
+
colorScheme?: ProgressBarColorScheme;
|
|
12
|
+
/**
|
|
13
|
+
* Sets the circular variant size. Does not affect the appearance of the linear variant.
|
|
14
|
+
* @default 'md'
|
|
15
|
+
*/
|
|
16
|
+
size?: ProgressBarSize;
|
|
17
|
+
/**
|
|
18
|
+
* The current progress value.
|
|
19
|
+
*/
|
|
20
|
+
value: number;
|
|
21
|
+
/**
|
|
22
|
+
* The minimum value.
|
|
23
|
+
* @default 0
|
|
24
|
+
*/
|
|
25
|
+
min?: number;
|
|
26
|
+
/**
|
|
27
|
+
* The maximum value.
|
|
28
|
+
* @default 100
|
|
29
|
+
*/
|
|
30
|
+
max?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Required text label for the progress bar.
|
|
33
|
+
*/
|
|
34
|
+
label: string;
|
|
35
|
+
/**
|
|
36
|
+
* Visually hide the label and value text.
|
|
37
|
+
*/
|
|
38
|
+
hideLabel?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Override the default percentage value label formatting.
|
|
41
|
+
*/
|
|
42
|
+
formatValueText?: (value: number, meta: {
|
|
43
|
+
min: number;
|
|
44
|
+
max: number;
|
|
45
|
+
percent: number;
|
|
46
|
+
}) => string;
|
|
47
|
+
/**
|
|
48
|
+
* A human-readable text alternative for the current value.
|
|
49
|
+
*/
|
|
50
|
+
'aria-valuetext'?: string;
|
|
51
|
+
}
|
|
52
|
+
export interface ProgressBarInternalProps {
|
|
53
|
+
percent: number;
|
|
54
|
+
label: string;
|
|
55
|
+
valueText: string;
|
|
56
|
+
hideLabel?: boolean;
|
|
57
|
+
colorScheme: ProgressBarColorScheme;
|
|
58
|
+
size: ProgressBarSize;
|
|
59
|
+
}
|
|
60
|
+
export default ProgressBarProps;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ProgressBarInternalProps } from './ProgressBar.props';
|
|
2
|
+
declare const ProgressBarCircular: {
|
|
3
|
+
({ percent, label, valueText, hideLabel, colorScheme, size, }: ProgressBarInternalProps): import("react/jsx-runtime").JSX.Element;
|
|
4
|
+
displayName: string;
|
|
5
|
+
};
|
|
6
|
+
export default ProgressBarCircular;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
3
|
+
import { Text, View } from 'react-native';
|
|
4
|
+
import Animated, { Easing, useAnimatedProps, useReducedMotion, useSharedValue, withTiming, } from 'react-native-reanimated';
|
|
5
|
+
import { Circle, G, Svg } from 'react-native-svg';
|
|
6
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
7
|
+
import useTheme from '../../hooks/useTheme';
|
|
8
|
+
import { BodyText } from '../BodyText';
|
|
9
|
+
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
|
|
10
|
+
const ProgressBarCircular = ({ percent, label, valueText, hideLabel, colorScheme, size, }) => {
|
|
11
|
+
const { components } = useTheme();
|
|
12
|
+
const isReducedMotion = useReducedMotion();
|
|
13
|
+
const progress = useSharedValue(0);
|
|
14
|
+
const hasMountedRef = useRef(false);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const target = Math.max(0, Math.min(100, percent)) / 100;
|
|
17
|
+
if (isReducedMotion) {
|
|
18
|
+
progress.value = target;
|
|
19
|
+
hasMountedRef.current = true;
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (!hasMountedRef.current) {
|
|
23
|
+
progress.value = target;
|
|
24
|
+
hasMountedRef.current = true;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
progress.value = withTiming(target, { duration: 300, easing: Easing.out(Easing.ease) });
|
|
28
|
+
}, [percent, isReducedMotion, progress]);
|
|
29
|
+
const circularTokens = components.progressBar.circular[size];
|
|
30
|
+
const barWidth = 'bar' in circularTokens ? circularTokens.bar.width : circularTokens.barWidth;
|
|
31
|
+
const diameter = circularTokens.height;
|
|
32
|
+
const radius = (diameter - barWidth) / 2;
|
|
33
|
+
const circumference = 2 * Math.PI * radius;
|
|
34
|
+
const animatedCircleProps = useAnimatedProps(() => ({
|
|
35
|
+
strokeDashoffset: circumference * (1 - progress.value),
|
|
36
|
+
}));
|
|
37
|
+
const indicatorColor = colorScheme === 'success'
|
|
38
|
+
? components.progressBar.progress.successColor
|
|
39
|
+
: colorScheme === 'danger'
|
|
40
|
+
? components.progressBar.progress.dangerColor
|
|
41
|
+
: components.progressBar.progress.defaultColor;
|
|
42
|
+
styles.useVariants({ size });
|
|
43
|
+
return (_jsxs(View, { style: styles.container, children: [_jsx(Svg, { width: diameter, height: diameter, viewBox: `0 0 ${diameter} ${diameter}`, style: styles.svg, children: _jsxs(G, { origin: `${diameter / 2}, ${diameter / 2}`, rotation: -90, children: [_jsx(Circle, { cx: "50%", cy: "50%", r: radius, stroke: components.progressBar.barColor, strokeWidth: barWidth, fill: "transparent" }), _jsx(AnimatedCircle, { cx: "50%", cy: "50%", r: radius, stroke: indicatorColor, strokeWidth: barWidth, fill: "transparent", strokeLinecap: "round", strokeDasharray: circumference, animatedProps: animatedCircleProps })] }) }), _jsxs(View, { style: styles.content, children: [_jsx(Text, { style: styles.valueText, children: valueText }), !hideLabel && size === 'md' ? (_jsx(BodyText, { style: styles.label, size: "md", weight: "semibold", children: label })) : null] })] }));
|
|
44
|
+
};
|
|
45
|
+
ProgressBarCircular.displayName = 'ProgressBarCircular';
|
|
46
|
+
const styles = StyleSheet.create(theme => ({
|
|
47
|
+
container: {
|
|
48
|
+
alignItems: 'center',
|
|
49
|
+
justifyContent: 'center',
|
|
50
|
+
position: 'relative',
|
|
51
|
+
variants: {
|
|
52
|
+
size: {
|
|
53
|
+
md: {
|
|
54
|
+
width: theme.components.progressBar.circular.md.height,
|
|
55
|
+
height: theme.components.progressBar.circular.md.height,
|
|
56
|
+
},
|
|
57
|
+
sm: {
|
|
58
|
+
width: theme.components.progressBar.circular.sm.height,
|
|
59
|
+
height: theme.components.progressBar.circular.sm.height,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
svg: {
|
|
65
|
+
position: 'absolute',
|
|
66
|
+
top: 0,
|
|
67
|
+
left: 0,
|
|
68
|
+
},
|
|
69
|
+
content: {
|
|
70
|
+
alignItems: 'center',
|
|
71
|
+
justifyContent: 'center',
|
|
72
|
+
_web: {
|
|
73
|
+
position: 'absolute',
|
|
74
|
+
top: 0,
|
|
75
|
+
left: 0,
|
|
76
|
+
width: '100%',
|
|
77
|
+
height: '100%',
|
|
78
|
+
},
|
|
79
|
+
variants: {
|
|
80
|
+
size: {
|
|
81
|
+
md: {
|
|
82
|
+
gap: theme.components.progressBar.circular.md.gap,
|
|
83
|
+
},
|
|
84
|
+
sm: {
|
|
85
|
+
gap: 0,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
valueText: {
|
|
91
|
+
color: theme.color.text.primary,
|
|
92
|
+
textAlign: 'center',
|
|
93
|
+
variants: {
|
|
94
|
+
size: {
|
|
95
|
+
md: {
|
|
96
|
+
fontFamily: theme.components.progressBar.circular.md.label.fontFamily,
|
|
97
|
+
fontSize: theme.components.progressBar.circular.md.label.fontSize,
|
|
98
|
+
lineHeight: theme.components.progressBar.circular.md.label.lineHeight,
|
|
99
|
+
fontWeight: theme.components.progressBar.circular.md.label.fontWeight,
|
|
100
|
+
},
|
|
101
|
+
sm: {
|
|
102
|
+
fontFamily: theme.typography.mobile.bodyText.fontFamily,
|
|
103
|
+
fontSize: theme.typography.mobile.bodyText.md.fontSize,
|
|
104
|
+
lineHeight: theme.typography.mobile.bodyText.md.lineHeight,
|
|
105
|
+
fontWeight: theme.fontWeight.semibold,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
label: {
|
|
111
|
+
textAlign: 'center',
|
|
112
|
+
maxWidth: 90,
|
|
113
|
+
},
|
|
114
|
+
}));
|
|
115
|
+
export default ProgressBarCircular;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ProgressBarInternalProps } from './ProgressBar.props';
|
|
2
|
+
declare const ProgressBarLinear: {
|
|
3
|
+
({ percent, label, valueText, hideLabel, colorScheme, }: ProgressBarInternalProps): import("react/jsx-runtime").JSX.Element;
|
|
4
|
+
displayName: string;
|
|
5
|
+
};
|
|
6
|
+
export default ProgressBarLinear;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { Platform, View } from 'react-native';
|
|
4
|
+
import Animated, { Easing, useAnimatedStyle, useReducedMotion, useSharedValue, withTiming, } from 'react-native-reanimated';
|
|
5
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
6
|
+
import useTheme from '../../hooks/useTheme';
|
|
7
|
+
import { BodyText } from '../BodyText';
|
|
8
|
+
const ProgressBarLinear = ({ percent, label, valueText, hideLabel, colorScheme, }) => {
|
|
9
|
+
const { components } = useTheme();
|
|
10
|
+
const isReducedMotion = useReducedMotion();
|
|
11
|
+
const progress = useSharedValue(0);
|
|
12
|
+
const hasMountedRef = useRef(false);
|
|
13
|
+
const [trackWidth, setTrackWidth] = useState(0);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const target = Math.max(0, Math.min(100, percent)) / 100;
|
|
16
|
+
if (isReducedMotion) {
|
|
17
|
+
progress.value = target;
|
|
18
|
+
hasMountedRef.current = true;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (!hasMountedRef.current) {
|
|
22
|
+
progress.value = target;
|
|
23
|
+
hasMountedRef.current = true;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
progress.value = withTiming(target, { duration: 300, easing: Easing.out(Easing.ease) });
|
|
27
|
+
}, [percent, isReducedMotion, progress]);
|
|
28
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
29
|
+
width: trackWidth * progress.value,
|
|
30
|
+
}));
|
|
31
|
+
const indicatorColor = colorScheme === 'success'
|
|
32
|
+
? components.progressBar.progress.successColor
|
|
33
|
+
: colorScheme === 'danger'
|
|
34
|
+
? components.progressBar.progress.dangerColor
|
|
35
|
+
: components.progressBar.progress.defaultColor;
|
|
36
|
+
const handleTrackLayout = (event) => {
|
|
37
|
+
setTrackWidth(event.nativeEvent.layout.width);
|
|
38
|
+
};
|
|
39
|
+
return (_jsxs(View, { style: styles.container, children: [!hideLabel && (_jsxs(View, { style: styles.header, children: [_jsx(BodyText, { size: "md", weight: "semibold", style: styles.label, children: label }), _jsx(BodyText, { size: "md", style: styles.value, children: valueText })] })), _jsx(View, { style: styles.track, onLayout: handleTrackLayout, children: Platform.OS === 'web' ? (_jsx(View, { style: [
|
|
40
|
+
styles.indicator,
|
|
41
|
+
{ width: `${Math.max(0, Math.min(100, percent))}%` },
|
|
42
|
+
{ backgroundColor: indicatorColor },
|
|
43
|
+
] })) : (_jsx(Animated.View, { style: [styles.indicator, animatedStyle, { backgroundColor: indicatorColor }] })) })] }));
|
|
44
|
+
};
|
|
45
|
+
ProgressBarLinear.displayName = 'ProgressBarLinear';
|
|
46
|
+
const styles = StyleSheet.create(theme => ({
|
|
47
|
+
container: {
|
|
48
|
+
width: '100%',
|
|
49
|
+
gap: theme.components.progressBar.linear.gap,
|
|
50
|
+
},
|
|
51
|
+
header: {
|
|
52
|
+
flexDirection: 'row',
|
|
53
|
+
alignItems: 'center',
|
|
54
|
+
justifyContent: 'space-between',
|
|
55
|
+
gap: theme.components.progressBar.linear.label.gap,
|
|
56
|
+
},
|
|
57
|
+
label: {
|
|
58
|
+
flex: 1,
|
|
59
|
+
},
|
|
60
|
+
value: {
|
|
61
|
+
flexShrink: 0,
|
|
62
|
+
textAlign: 'right',
|
|
63
|
+
},
|
|
64
|
+
track: {
|
|
65
|
+
width: '100%',
|
|
66
|
+
height: theme.components.progressBar.linear.bar.height,
|
|
67
|
+
backgroundColor: theme.components.progressBar.barColor,
|
|
68
|
+
borderRadius: theme.components.progressBar.linear.bar.borderRadius,
|
|
69
|
+
overflow: 'hidden',
|
|
70
|
+
},
|
|
71
|
+
indicator: {
|
|
72
|
+
height: '100%',
|
|
73
|
+
borderRadius: theme.components.progressBar.linear.bar.borderRadius,
|
|
74
|
+
_web: {
|
|
75
|
+
height: '100%',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
}));
|
|
79
|
+
export default ProgressBarLinear;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as ProgressBar } from './ProgressBar';
|
|
@@ -74,6 +74,7 @@ import {
|
|
|
74
74
|
OL,
|
|
75
75
|
Pill,
|
|
76
76
|
PillGroup,
|
|
77
|
+
ProgressBar,
|
|
77
78
|
ProgressStep,
|
|
78
79
|
ProgressStepper,
|
|
79
80
|
Radio,
|
|
@@ -663,6 +664,11 @@ const AllComponents: React.FC = () => {
|
|
|
663
664
|
</PillGroup>
|
|
664
665
|
</Center>
|
|
665
666
|
</ComponentWrapper>
|
|
667
|
+
<ComponentWrapper name="Progress Bar" link="/?path=/docs/components-progress-bar--docs">
|
|
668
|
+
<Center flex={1} px="300">
|
|
669
|
+
<ProgressBar value={58} label="Order progress" />
|
|
670
|
+
</Center>
|
|
671
|
+
</ComponentWrapper>
|
|
666
672
|
<ComponentWrapper
|
|
667
673
|
name="Progress Stepper"
|
|
668
674
|
link="/?path=/docs/components-progress-stepper--docs"
|
package/package.json
CHANGED
|
@@ -4,16 +4,14 @@ import { BottomSheetProps } from '../BottomSheet';
|
|
|
4
4
|
import { ButtonWithoutChildrenProps } from '../Button/Button.props';
|
|
5
5
|
import { UnstyledIconButtonProps } from '../UnstyledIconButton';
|
|
6
6
|
|
|
7
|
-
interface
|
|
7
|
+
interface ModalPropsBase extends Omit<BottomSheetProps, 'children'> {
|
|
8
8
|
loading?: boolean;
|
|
9
9
|
image?: ReactNode;
|
|
10
10
|
showCloseButton?: boolean;
|
|
11
11
|
heading?: string;
|
|
12
12
|
loadingHeading?: string;
|
|
13
13
|
description?: string;
|
|
14
|
-
inNavModal?: boolean;
|
|
15
14
|
fullscreen?: boolean;
|
|
16
|
-
stickyFooter?: boolean;
|
|
17
15
|
children?: ViewProps['children'];
|
|
18
16
|
onPressPrimaryButton?: () => void;
|
|
19
17
|
primaryButtonText?: string;
|
|
@@ -25,7 +23,18 @@ interface ModalProps extends Omit<BottomSheetProps, 'children'> {
|
|
|
25
23
|
primaryButtonProps?: Omit<ButtonWithoutChildrenProps, 'children'>;
|
|
26
24
|
secondaryButtonProps?: Omit<ButtonWithoutChildrenProps, 'children'>;
|
|
27
25
|
closeButtonProps?: Omit<UnstyledIconButtonProps, 'children'>;
|
|
28
|
-
background?: 'default' | 'brand';
|
|
29
26
|
}
|
|
30
27
|
|
|
28
|
+
type ModalProps =
|
|
29
|
+
| (ModalPropsBase & {
|
|
30
|
+
inNavModal?: false | undefined;
|
|
31
|
+
stickyFooter?: boolean;
|
|
32
|
+
background?: never;
|
|
33
|
+
})
|
|
34
|
+
| (ModalPropsBase & {
|
|
35
|
+
inNavModal: true;
|
|
36
|
+
stickyFooter?: never;
|
|
37
|
+
background?: 'default' | 'brand';
|
|
38
|
+
});
|
|
39
|
+
|
|
31
40
|
export default ModalProps;
|