@utilitywarehouse/hearth-react-native 0.25.0 → 0.27.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 +15 -15
- package/CHANGELOG.md +71 -0
- package/build/components/Banner/Banner.js +12 -1
- package/build/components/Modal/Modal.d.ts +1 -1
- package/build/components/Modal/Modal.js +30 -7
- package/build/components/Modal/Modal.props.d.ts +4 -2
- package/build/components/PillGroup/Pill.js +0 -1
- package/build/components/PillGroup/PillGroup.js +4 -1
- package/build/components/SegmentedControl/SegmentedControl.context.d.ts +14 -0
- package/build/components/SegmentedControl/SegmentedControl.context.js +9 -0
- package/build/components/SegmentedControl/SegmentedControl.d.ts +6 -0
- package/build/components/SegmentedControl/SegmentedControl.js +196 -0
- package/build/components/SegmentedControl/SegmentedControl.props.d.ts +18 -0
- package/build/components/SegmentedControl/SegmentedControl.props.js +1 -0
- package/build/components/SegmentedControl/SegmentedControlOption.d.ts +18 -0
- package/build/components/SegmentedControl/SegmentedControlOption.js +122 -0
- package/build/components/SegmentedControl/SegmentedControlOption.props.d.ts +12 -0
- package/build/components/SegmentedControl/SegmentedControlOption.props.js +1 -0
- package/build/components/SegmentedControl/index.d.ts +4 -0
- package/build/components/SegmentedControl/index.js +2 -0
- package/build/components/index.d.ts +1 -0
- package/build/components/index.js +1 -0
- package/docs/changelog.mdx +136 -0
- package/docs/components/AllComponents.web.tsx +14 -0
- package/package.json +3 -3
- package/src/components/Banner/Banner.tsx +12 -1
- package/src/components/Modal/Modal.docs.mdx +9 -3
- package/src/components/Modal/Modal.props.ts +4 -2
- package/src/components/Modal/Modal.tsx +44 -7
- package/src/components/PillGroup/Pill.tsx +0 -1
- package/src/components/PillGroup/PillGroup.tsx +4 -0
- package/src/components/SegmentedControl/SegmentedControl.context.ts +22 -0
- package/src/components/SegmentedControl/SegmentedControl.docs.mdx +90 -0
- package/src/components/SegmentedControl/SegmentedControl.figma.tsx +40 -0
- package/src/components/SegmentedControl/SegmentedControl.props.ts +20 -0
- package/src/components/SegmentedControl/SegmentedControl.stories.tsx +77 -0
- package/src/components/SegmentedControl/SegmentedControl.tsx +257 -0
- package/src/components/SegmentedControl/SegmentedControlOption.props.ts +14 -0
- package/src/components/SegmentedControl/SegmentedControlOption.tsx +213 -0
- package/src/components/SegmentedControl/index.ts +4 -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.27.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
|
+
86: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
|
+
313: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
|
|
@@ -58,15 +58,15 @@
|
|
|
58
58
|
|
|
59
59
|
✖ 25 problems (0 errors, 25 warnings)
|
|
60
60
|
|
|
61
|
-
Rule
|
|
62
|
-
|
|
63
|
-
@typescript-eslint/no-unused-vars
|
|
64
|
-
react-hooks/exhaustive-deps
|
|
65
|
-
no-global-assign
|
|
66
|
-
react-hooks/rules-of-hooks
|
|
67
|
-
|
|
68
|
-
no-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
no-
|
|
72
|
-
|
|
61
|
+
Rule | Time (ms) | Relative
|
|
62
|
+
:---------------------------------|----------:|--------:
|
|
63
|
+
@typescript-eslint/no-unused-vars | 1531.354 | 62.0%
|
|
64
|
+
react-hooks/exhaustive-deps | 115.862 | 4.7%
|
|
65
|
+
no-global-assign | 82.745 | 3.3%
|
|
66
|
+
react-hooks/rules-of-hooks | 81.422 | 3.3%
|
|
67
|
+
@typescript-eslint/ban-ts-comment | 54.976 | 2.2%
|
|
68
|
+
no-misleading-character-class | 43.812 | 1.8%
|
|
69
|
+
no-unexpected-multiline | 39.804 | 1.6%
|
|
70
|
+
no-fallthrough | 33.437 | 1.4%
|
|
71
|
+
no-regex-spaces | 28.539 | 1.2%
|
|
72
|
+
no-shadow-restricted-names | 25.451 | 1.0%
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,76 @@
|
|
|
1
1
|
# @utilitywarehouse/hearth-react-native
|
|
2
2
|
|
|
3
|
+
## 0.27.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#987](https://github.com/utilitywarehouse/hearth/pull/987) [`eb962d2`](https://github.com/utilitywarehouse/hearth/commit/eb962d2f33b63fa3aeda0b291fd41ace90d04c41) Thanks [@jordmccord](https://github.com/jordmccord)! - 🌟 [FEATURE]: Add `SegmentedControl` and `SegmentedControlOption` components.
|
|
8
|
+
|
|
9
|
+
This introduces a new segmented control component for switching between a small set of related options.
|
|
10
|
+
The component includes controlled and uncontrolled usage, size variants (`sm`, `md`), animated selected indicator movement, and improved accessibility semantics for screen readers.
|
|
11
|
+
|
|
12
|
+
**Components affected**:
|
|
13
|
+
- `SegmentedControl`
|
|
14
|
+
- `SegmentedControlOption`
|
|
15
|
+
|
|
16
|
+
**Developer changes**:
|
|
17
|
+
|
|
18
|
+
Import and compose the new components as follows:
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
import { SegmentedControl, SegmentedControlOption } from '@utilitywarehouse/hearth-react-native';
|
|
22
|
+
|
|
23
|
+
<SegmentedControl defaultValue="day" size="sm">
|
|
24
|
+
<SegmentedControlOption value="day">Day</SegmentedControlOption>
|
|
25
|
+
<SegmentedControlOption value="week">Week</SegmentedControlOption>
|
|
26
|
+
<SegmentedControlOption value="month">Month</SegmentedControlOption>
|
|
27
|
+
</SegmentedControl>;
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Patch Changes
|
|
31
|
+
|
|
32
|
+
- [#989](https://github.com/utilitywarehouse/hearth/pull/989) [`c97122e`](https://github.com/utilitywarehouse/hearth/commit/c97122eb429ec4adef656fb245a9256a5619df61) Thanks [@jordmccord](https://github.com/jordmccord)! - 🐛 [FIX]: Ensure horizontal `Banner` fills available width when `onPress` is not provided.
|
|
33
|
+
|
|
34
|
+
Fixed a layout issue where a horizontal `Banner` without `onPress` could fail to stretch correctly within its parent container.
|
|
35
|
+
|
|
36
|
+
**Components affected**:
|
|
37
|
+
- `Banner`
|
|
38
|
+
|
|
39
|
+
**Developer changes**:
|
|
40
|
+
|
|
41
|
+
No changes required.
|
|
42
|
+
|
|
43
|
+
## 0.26.0
|
|
44
|
+
|
|
45
|
+
### Minor Changes
|
|
46
|
+
|
|
47
|
+
- [#981](https://github.com/utilitywarehouse/hearth/pull/981) [`df56387`](https://github.com/utilitywarehouse/hearth/commit/df563872e6bf040d419f6c7fce2343ebe560edb9) Thanks [@declanelcocks](https://github.com/declanelcocks)! - 🌟 [ENHANCEMENT]: Improve `Modal` behaviour when used inside a React Navigation modal (`inNavModal`).
|
|
48
|
+
|
|
49
|
+
The following improvements have been made to the `Modal` component when used in a navigation context with `inNavModal={true}`:
|
|
50
|
+
- **`scrollable` prop**: Content is now rendered inside a `ScrollView` by default. Set `scrollable={false}` to opt out, for example when you need to centre content or use a custom layout.
|
|
51
|
+
- **`stickyFooter` support**: The `stickyFooter` prop now works correctly in `inNavModal` mode.
|
|
52
|
+
- **Auto full-screen detection**: When the modal fills the entire screen (e.g. with `presentation: 'fullScreenModal'`), the `fullscreen` style is applied automatically. The `fullscreen` prop is no longer available when `inNavModal` is `true`.
|
|
53
|
+
|
|
54
|
+
**Components affected**:
|
|
55
|
+
- `Modal`
|
|
56
|
+
|
|
57
|
+
**Developer changes**:
|
|
58
|
+
|
|
59
|
+
No changes are required for existing usage. If you were passing `fullscreen` alongside `inNavModal={true}`, remove the `fullscreen` prop — full-screen styling is now detected automatically:
|
|
60
|
+
|
|
61
|
+
```diff
|
|
62
|
+
- <Modal inNavModal fullscreen>
|
|
63
|
+
+ <Modal inNavModal>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
To disable the default `ScrollView` wrapping in `inNavModal` mode:
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
<Modal inNavModal scrollable={false}>
|
|
70
|
+
{/* custom layout */}
|
|
71
|
+
</Modal>
|
|
72
|
+
```
|
|
73
|
+
|
|
3
74
|
## 0.25.0
|
|
4
75
|
|
|
5
76
|
### Minor Changes
|
|
@@ -11,7 +11,7 @@ import { UnstyledIconButton } from '../UnstyledIconButton';
|
|
|
11
11
|
import BannerContext from './Banner.context';
|
|
12
12
|
const Banner = ({ icon, iconContainerVariant = 'subtle', iconContainerSize = 'md', iconContainerColor = 'pig', illustration, image, heading, description, direction = 'horizontal', link, button, onPress, onClose, variant = 'subtle', style, ...props }) => {
|
|
13
13
|
const hasIllustration = Boolean(illustration);
|
|
14
|
-
styles.useVariants({ direction, hasIllustration });
|
|
14
|
+
styles.useVariants({ direction, hasIllustration, isPressable: Boolean(onPress) });
|
|
15
15
|
const context = useMemo(() => ({
|
|
16
16
|
direction,
|
|
17
17
|
}), [direction]);
|
|
@@ -66,6 +66,10 @@ const styles = StyleSheet.create(theme => ({
|
|
|
66
66
|
true: {},
|
|
67
67
|
false: {},
|
|
68
68
|
},
|
|
69
|
+
isPressable: {
|
|
70
|
+
true: {},
|
|
71
|
+
false: {},
|
|
72
|
+
},
|
|
69
73
|
},
|
|
70
74
|
compoundVariants: [
|
|
71
75
|
{
|
|
@@ -82,6 +86,13 @@ const styles = StyleSheet.create(theme => ({
|
|
|
82
86
|
alignItems: 'center',
|
|
83
87
|
},
|
|
84
88
|
},
|
|
89
|
+
{
|
|
90
|
+
direction: 'horizontal',
|
|
91
|
+
isPressable: false,
|
|
92
|
+
styles: {
|
|
93
|
+
flex: 1,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
85
96
|
],
|
|
86
97
|
},
|
|
87
98
|
media: {
|
|
@@ -3,5 +3,5 @@ import ModalProps from './Modal.props';
|
|
|
3
3
|
type Modal<T = any> = BottomSheetModalMethods<T> & {
|
|
4
4
|
triggerCloseAnimation?: () => void;
|
|
5
5
|
};
|
|
6
|
-
declare const Modal: ({ ref, children, heading, description, showCloseButton, primaryButtonText, secondaryButtonText, onPressPrimaryButton, onPressCloseButton, onPressSecondaryButton, closeOnPrimaryButtonPress, closeOnSecondaryButtonPress, loading, loadingHeading, fullscreen, image, primaryButtonProps, secondaryButtonProps, closeButtonProps, inNavModal, stickyFooter, background, ...props }: ModalProps) => import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
declare const Modal: ({ ref, children, heading, description, showCloseButton, primaryButtonText, secondaryButtonText, onPressPrimaryButton, onPressCloseButton, onPressSecondaryButton, closeOnPrimaryButtonPress, closeOnSecondaryButtonPress, loading, loadingHeading, fullscreen, image, primaryButtonProps, secondaryButtonProps, closeButtonProps, inNavModal, stickyFooter, background, scrollable, ...props }: ModalProps) => import("react/jsx-runtime").JSX.Element;
|
|
7
7
|
export default Modal;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { BottomSheetFooter, } from '@gorhom/bottom-sheet';
|
|
3
3
|
import { CloseMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
|
|
4
|
-
import { useCallback, useEffect, useImperativeHandle, useRef } from 'react';
|
|
5
|
-
import { AccessibilityInfo, Platform, ScrollView, View, findNodeHandle } from 'react-native';
|
|
4
|
+
import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
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';
|
|
@@ -13,7 +13,7 @@ import { Button } from '../Button';
|
|
|
13
13
|
import { Heading } from '../Heading';
|
|
14
14
|
import { Spinner } from '../Spinner';
|
|
15
15
|
import { UnstyledIconButton } from '../UnstyledIconButton';
|
|
16
|
-
const Modal = ({ ref, children, heading, description, showCloseButton = true, primaryButtonText, secondaryButtonText, onPressPrimaryButton, onPressCloseButton, onPressSecondaryButton, closeOnPrimaryButtonPress = true, closeOnSecondaryButtonPress = true, loading, loadingHeading = 'Loading...', fullscreen = false, image, primaryButtonProps, secondaryButtonProps, closeButtonProps, inNavModal = false, stickyFooter = true, background = 'default', ...props }) => {
|
|
16
|
+
const Modal = ({ ref, children, heading, description, showCloseButton = true, primaryButtonText, secondaryButtonText, onPressPrimaryButton, onPressCloseButton, onPressSecondaryButton, closeOnPrimaryButtonPress = true, closeOnSecondaryButtonPress = true, loading, loadingHeading = 'Loading...', fullscreen = false, image, primaryButtonProps, secondaryButtonProps, closeButtonProps, inNavModal = false, stickyFooter = true, background = 'default', scrollable = true, ...props }) => {
|
|
17
17
|
const bottomSheetModalRef = useRef(null);
|
|
18
18
|
const viewRef = useRef(null);
|
|
19
19
|
const scrollViewRef = useRef(null);
|
|
@@ -21,6 +21,13 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
|
|
|
21
21
|
const backgroundOpacity = useSharedValue(0);
|
|
22
22
|
const pretendContentTranslateY = useSharedValue(20);
|
|
23
23
|
const isBrandBackground = background === 'brand';
|
|
24
|
+
const [inNavModalHeight, setInNavModalHeight] = useState();
|
|
25
|
+
const isNavModalFullScreen = useMemo(() => {
|
|
26
|
+
if (!inNavModalHeight || !inNavModal)
|
|
27
|
+
return false;
|
|
28
|
+
const screenHeight = Dimensions.get('window').height;
|
|
29
|
+
return inNavModalHeight >= screenHeight;
|
|
30
|
+
}, [inNavModalHeight, inNavModal]);
|
|
24
31
|
const triggerCloseAnimation = useCallback(() => {
|
|
25
32
|
if (Platform.OS === 'android' && inNavModal) {
|
|
26
33
|
pretendContentTranslateY.value = withTiming(20, {
|
|
@@ -107,9 +114,13 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
|
|
|
107
114
|
stickyFooter,
|
|
108
115
|
showHandle: props.showHandle,
|
|
109
116
|
background: isBrandBackground ? 'brand' : 'primary',
|
|
117
|
+
...(inNavModal && {
|
|
118
|
+
fullscreen: isNavModalFullScreen,
|
|
119
|
+
}),
|
|
110
120
|
});
|
|
111
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] }));
|
|
112
|
-
const
|
|
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: { flexGrow: stickyFooter ? 1 : 0 }, children: [children, !stickyFooter ? _jsx(View, { style: styles.inNavModalFooterContainer, children: footer }) : null] })), !inNavModal && children, ((!stickyFooter && !inNavModal) || (inNavModal && stickyFooter)) && !noButtons ? footer : null] })) }));
|
|
113
124
|
const renderFooter = useCallback((props) => (_jsx(BottomSheetFooter, { ...props, children: _jsx(View, { style: styles.footerWrap, children: footer }) })), [
|
|
114
125
|
onPressPrimaryButton,
|
|
115
126
|
primaryButtonText,
|
|
@@ -118,7 +129,9 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
|
|
|
118
129
|
primaryButtonProps,
|
|
119
130
|
secondaryButtonProps,
|
|
120
131
|
]);
|
|
121
|
-
return inNavModal ? (_jsxs(View, {
|
|
132
|
+
return inNavModal ? (_jsxs(View, { onLayout: (e) => {
|
|
133
|
+
setInNavModalHeight(e.nativeEvent.layout.height);
|
|
134
|
+
}, style: {
|
|
122
135
|
flex: 1,
|
|
123
136
|
backgroundColor: theme.color.background[isBrandBackground ? 'brand' : 'primary'],
|
|
124
137
|
}, 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 })] }));
|
|
@@ -227,8 +240,6 @@ const styles = StyleSheet.create((theme, rt) => ({
|
|
|
227
240
|
borderTopLeftRadius: theme.components.modal.borderRadius,
|
|
228
241
|
borderTopRightRadius: theme.components.modal.borderRadius,
|
|
229
242
|
backgroundColor: theme.color.surface.neutral.strong,
|
|
230
|
-
gap: theme.components.modal.gap,
|
|
231
|
-
padding: theme.components.modal.padding,
|
|
232
243
|
paddingBottom: theme.components.modal.padding + rt.insets.bottom,
|
|
233
244
|
variants: {
|
|
234
245
|
background: {
|
|
@@ -237,8 +248,20 @@ const styles = StyleSheet.create((theme, rt) => ({
|
|
|
237
248
|
backgroundColor: theme.color.background.brand,
|
|
238
249
|
},
|
|
239
250
|
},
|
|
251
|
+
fullscreen: {
|
|
252
|
+
true: {
|
|
253
|
+
padding: theme.components.modal.padding,
|
|
254
|
+
paddingTop: rt.insets.top,
|
|
255
|
+
},
|
|
256
|
+
false: {
|
|
257
|
+
padding: theme.components.modal.padding,
|
|
258
|
+
}
|
|
259
|
+
}
|
|
240
260
|
},
|
|
241
261
|
},
|
|
262
|
+
inNavModalFooterContainer: {
|
|
263
|
+
paddingTop: theme.components.modal.padding,
|
|
264
|
+
},
|
|
242
265
|
androidContainer: {
|
|
243
266
|
height: rt.insets.top + 18,
|
|
244
267
|
paddingLeft: theme.components.modal.padding,
|
|
@@ -11,6 +11,7 @@ interface ModalPropsBase extends Omit<BottomSheetProps, 'children'> {
|
|
|
11
11
|
loadingHeading?: string;
|
|
12
12
|
description?: string;
|
|
13
13
|
fullscreen?: boolean;
|
|
14
|
+
stickyFooter?: boolean;
|
|
14
15
|
children?: ViewProps['children'];
|
|
15
16
|
onPressPrimaryButton?: () => void;
|
|
16
17
|
primaryButtonText?: string;
|
|
@@ -25,11 +26,12 @@ interface ModalPropsBase extends Omit<BottomSheetProps, 'children'> {
|
|
|
25
26
|
}
|
|
26
27
|
type ModalProps = (ModalPropsBase & {
|
|
27
28
|
inNavModal?: false | undefined;
|
|
28
|
-
|
|
29
|
+
scrollable?: never;
|
|
29
30
|
background?: never;
|
|
30
31
|
}) | (ModalPropsBase & {
|
|
31
32
|
inNavModal: true;
|
|
32
|
-
|
|
33
|
+
fullscreen?: never;
|
|
34
|
+
scrollable?: boolean;
|
|
33
35
|
background?: 'default' | 'brand';
|
|
34
36
|
});
|
|
35
37
|
export default ModalProps;
|
|
@@ -20,10 +20,13 @@ export const PillGroup = ({ children, value, multiple = false, wrap = true, onCh
|
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
}), [normalizedValue, multiple, onChange]);
|
|
23
|
-
return (_jsx(PillGroupContext.Provider, { value: contextValue, children: wrap ? (_jsx(Box, { style: [styles.group, styles.wrap, style], ...props, children: children })) : (_jsx(ScrollView, { horizontal: true, contentContainerStyle: [styles.group, style], showsHorizontalScrollIndicator: false, ...props, children: children })) }));
|
|
23
|
+
return (_jsx(PillGroupContext.Provider, { value: contextValue, children: wrap ? (_jsx(Box, { style: [styles.group, styles.wrap, style], ...props, children: children })) : (_jsx(ScrollView, { horizontal: true, style: styles.scrollView, contentContainerStyle: [styles.group, style], showsHorizontalScrollIndicator: false, ...props, children: children })) }));
|
|
24
24
|
};
|
|
25
25
|
PillGroup.displayName = 'PillGroup';
|
|
26
26
|
const styles = StyleSheet.create(theme => ({
|
|
27
|
+
scrollView: {
|
|
28
|
+
flexGrow: 0,
|
|
29
|
+
},
|
|
27
30
|
group: {
|
|
28
31
|
flexDirection: 'row',
|
|
29
32
|
gap: theme.components.pill.group.gap,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type SegmentedControlContextValue = {
|
|
2
|
+
value?: string;
|
|
3
|
+
select: (value: string) => void;
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
size: 'sm' | 'md';
|
|
6
|
+
registerOptionLayout: (value: string, layout: {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
}) => void;
|
|
12
|
+
};
|
|
13
|
+
export declare const SegmentedControlContext: import("react").Context<SegmentedControlContextValue | null>;
|
|
14
|
+
export declare const useSegmentedControlContext: () => SegmentedControlContextValue;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
export const SegmentedControlContext = createContext(null);
|
|
3
|
+
export const useSegmentedControlContext = () => {
|
|
4
|
+
const context = useContext(SegmentedControlContext);
|
|
5
|
+
if (!context) {
|
|
6
|
+
throw new Error('SegmentedControlOption must be used within SegmentedControl');
|
|
7
|
+
}
|
|
8
|
+
return context;
|
|
9
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type SegmentedControlProps from './SegmentedControl.props';
|
|
2
|
+
declare const SegmentedControl: {
|
|
3
|
+
({ value: controlledValue, defaultValue, onValueChange, size, disabled, children, style, ...props }: SegmentedControlProps): import("react/jsx-runtime").JSX.Element;
|
|
4
|
+
displayName: string;
|
|
5
|
+
};
|
|
6
|
+
export default SegmentedControl;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Children, isValidElement, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { 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 { useStyleProps } from '../../hooks';
|
|
7
|
+
import { SegmentedControlContext } from './SegmentedControl.context';
|
|
8
|
+
const Indicator = Animated.createAnimatedComponent(View);
|
|
9
|
+
const GROUP_BORDER_WIDTH = 1;
|
|
10
|
+
const SegmentedControl = ({ value: controlledValue, defaultValue, onValueChange, size = 'sm', disabled = false, children, style, ...props }) => {
|
|
11
|
+
const { computedStyles, remainingProps } = useStyleProps(props);
|
|
12
|
+
const isReducedMotion = useReducedMotion();
|
|
13
|
+
const indicatorPositionOffset = GROUP_BORDER_WIDTH;
|
|
14
|
+
const optionValues = useMemo(() => {
|
|
15
|
+
const values = [];
|
|
16
|
+
const walk = (node) => {
|
|
17
|
+
Children.forEach(node, child => {
|
|
18
|
+
if (!isValidElement(child))
|
|
19
|
+
return;
|
|
20
|
+
const childType = child.type;
|
|
21
|
+
const childProps = child.props;
|
|
22
|
+
if (childType?.displayName === 'SegmentedControlOption' &&
|
|
23
|
+
typeof childProps?.value === 'string') {
|
|
24
|
+
values.push(childProps.value);
|
|
25
|
+
}
|
|
26
|
+
if (childProps?.children) {
|
|
27
|
+
walk(childProps.children);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
walk(children);
|
|
32
|
+
return values;
|
|
33
|
+
}, [children]);
|
|
34
|
+
const optionValuesKey = useMemo(() => optionValues.join('|'), [optionValues]);
|
|
35
|
+
const optionValuesRef = useRef(optionValues);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
optionValuesRef.current = optionValues;
|
|
38
|
+
}, [optionValues]);
|
|
39
|
+
const getInitialValue = () => {
|
|
40
|
+
if (controlledValue !== undefined)
|
|
41
|
+
return controlledValue;
|
|
42
|
+
if (defaultValue !== undefined)
|
|
43
|
+
return defaultValue;
|
|
44
|
+
return optionValues[0];
|
|
45
|
+
};
|
|
46
|
+
const [uncontrolledValue, setUncontrolledValue] = useState(getInitialValue);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (controlledValue !== undefined) {
|
|
49
|
+
setUncontrolledValue(controlledValue);
|
|
50
|
+
}
|
|
51
|
+
}, [controlledValue]);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const currentOptionValues = optionValuesRef.current;
|
|
54
|
+
setUncontrolledValue(prev => {
|
|
55
|
+
if (!prev)
|
|
56
|
+
return currentOptionValues[0];
|
|
57
|
+
if (!currentOptionValues.includes(prev))
|
|
58
|
+
return currentOptionValues[0];
|
|
59
|
+
return prev;
|
|
60
|
+
});
|
|
61
|
+
}, [optionValuesKey]);
|
|
62
|
+
const currentValue = controlledValue !== undefined ? controlledValue : uncontrolledValue;
|
|
63
|
+
const indicatorX = useSharedValue(0);
|
|
64
|
+
const indicatorWidth = useSharedValue(0);
|
|
65
|
+
const indicatorY = useSharedValue(0);
|
|
66
|
+
const indicatorHeight = useSharedValue(0);
|
|
67
|
+
const [hasIndicator, setHasIndicator] = useState(false);
|
|
68
|
+
const layoutsRef = useRef(new Map());
|
|
69
|
+
const prevValueRef = useRef(undefined);
|
|
70
|
+
const initialisedRef = useRef(false);
|
|
71
|
+
const select = useCallback((nextValue) => {
|
|
72
|
+
if (disabled)
|
|
73
|
+
return;
|
|
74
|
+
if (controlledValue === undefined) {
|
|
75
|
+
setUncontrolledValue(nextValue);
|
|
76
|
+
}
|
|
77
|
+
onValueChange?.(nextValue);
|
|
78
|
+
}, [controlledValue, disabled, onValueChange]);
|
|
79
|
+
const registerOptionLayout = useCallback((value, layout) => {
|
|
80
|
+
layoutsRef.current.set(value, layout);
|
|
81
|
+
const activeValue = controlledValue !== undefined ? controlledValue : uncontrolledValue;
|
|
82
|
+
if (!activeValue || activeValue !== value)
|
|
83
|
+
return;
|
|
84
|
+
if (!initialisedRef.current) {
|
|
85
|
+
indicatorX.value = Math.max(0, layout.x - indicatorPositionOffset);
|
|
86
|
+
indicatorWidth.value = layout.width;
|
|
87
|
+
indicatorY.value = Math.max(0, layout.y - indicatorPositionOffset);
|
|
88
|
+
indicatorHeight.value = layout.height;
|
|
89
|
+
prevValueRef.current = activeValue;
|
|
90
|
+
initialisedRef.current = true;
|
|
91
|
+
setHasIndicator(true);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (prevValueRef.current === activeValue)
|
|
95
|
+
return;
|
|
96
|
+
const config = {
|
|
97
|
+
delay: 200,
|
|
98
|
+
duration: isReducedMotion ? 0 : 220,
|
|
99
|
+
easing: Easing.out(Easing.cubic),
|
|
100
|
+
};
|
|
101
|
+
indicatorX.value = withTiming(Math.max(0, layout.x - indicatorPositionOffset), config);
|
|
102
|
+
indicatorWidth.value = withTiming(layout.width, config);
|
|
103
|
+
indicatorY.value = withTiming(Math.max(0, layout.y - indicatorPositionOffset), config);
|
|
104
|
+
indicatorHeight.value = withTiming(layout.height, config);
|
|
105
|
+
prevValueRef.current = activeValue;
|
|
106
|
+
}, [
|
|
107
|
+
controlledValue,
|
|
108
|
+
indicatorHeight,
|
|
109
|
+
indicatorWidth,
|
|
110
|
+
indicatorX,
|
|
111
|
+
indicatorY,
|
|
112
|
+
indicatorPositionOffset,
|
|
113
|
+
isReducedMotion,
|
|
114
|
+
uncontrolledValue,
|
|
115
|
+
]);
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (!currentValue || !initialisedRef.current)
|
|
118
|
+
return;
|
|
119
|
+
if (prevValueRef.current === undefined || prevValueRef.current === currentValue)
|
|
120
|
+
return;
|
|
121
|
+
const layout = layoutsRef.current.get(currentValue);
|
|
122
|
+
if (!layout)
|
|
123
|
+
return;
|
|
124
|
+
const config = {
|
|
125
|
+
duration: isReducedMotion ? 0 : 220,
|
|
126
|
+
easing: Easing.out(Easing.cubic),
|
|
127
|
+
};
|
|
128
|
+
indicatorX.value = withTiming(Math.max(0, layout.x - indicatorPositionOffset), config);
|
|
129
|
+
indicatorWidth.value = withTiming(layout.width, config);
|
|
130
|
+
indicatorY.value = withTiming(Math.max(0, layout.y - indicatorPositionOffset), config);
|
|
131
|
+
indicatorHeight.value = withTiming(layout.height, config);
|
|
132
|
+
prevValueRef.current = currentValue;
|
|
133
|
+
}, [
|
|
134
|
+
currentValue,
|
|
135
|
+
indicatorHeight,
|
|
136
|
+
indicatorWidth,
|
|
137
|
+
indicatorX,
|
|
138
|
+
indicatorY,
|
|
139
|
+
indicatorPositionOffset,
|
|
140
|
+
isReducedMotion,
|
|
141
|
+
optionValuesKey,
|
|
142
|
+
]);
|
|
143
|
+
const indicatorStyle = useAnimatedStyle(() => ({
|
|
144
|
+
transform: [{ translateX: indicatorX.value }, { translateY: indicatorY.value }],
|
|
145
|
+
width: indicatorWidth.value,
|
|
146
|
+
height: indicatorHeight.value,
|
|
147
|
+
}));
|
|
148
|
+
styles.useVariants({ disabled, size });
|
|
149
|
+
const contextValue = useMemo(() => ({
|
|
150
|
+
value: currentValue,
|
|
151
|
+
select,
|
|
152
|
+
disabled,
|
|
153
|
+
size,
|
|
154
|
+
registerOptionLayout,
|
|
155
|
+
}), [currentValue, select, disabled, size, registerOptionLayout]);
|
|
156
|
+
return (_jsx(SegmentedControlContext.Provider, { value: contextValue, children: _jsxs(View, { accessibilityRole: "radiogroup", accessibilityState: { disabled }, style: [styles.container, computedStyles, style], ...remainingProps, children: [hasIndicator ? (_jsx(Indicator, { pointerEvents: "none", style: [styles.indicator, indicatorStyle] })) : null, children] }) }));
|
|
157
|
+
};
|
|
158
|
+
SegmentedControl.displayName = 'SegmentedControl';
|
|
159
|
+
const styles = StyleSheet.create(theme => ({
|
|
160
|
+
container: {
|
|
161
|
+
flexDirection: 'row',
|
|
162
|
+
alignItems: 'center',
|
|
163
|
+
alignSelf: 'flex-start',
|
|
164
|
+
gap: theme.components.segmentedControl.group.gap,
|
|
165
|
+
height: theme.components.segmentedControl.group.height,
|
|
166
|
+
borderRadius: theme.components.segmentedControl.group.borderRadius,
|
|
167
|
+
borderWidth: theme.components.segmentedControl.group.borderWidth,
|
|
168
|
+
backgroundColor: theme.color.surface.neutral.subtle,
|
|
169
|
+
borderColor: theme.color.border.strong,
|
|
170
|
+
variants: {
|
|
171
|
+
size: {
|
|
172
|
+
sm: {
|
|
173
|
+
height: 32,
|
|
174
|
+
padding: 2,
|
|
175
|
+
},
|
|
176
|
+
md: {
|
|
177
|
+
height: theme.components.segmentedControl.group.height,
|
|
178
|
+
padding: 2,
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
disabled: {
|
|
182
|
+
true: {
|
|
183
|
+
opacity: theme.opacity.disabled,
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
indicator: {
|
|
189
|
+
position: 'absolute',
|
|
190
|
+
left: 0,
|
|
191
|
+
top: 0,
|
|
192
|
+
borderRadius: theme.components.segmentedControl.borderRadius,
|
|
193
|
+
backgroundColor: theme.color.interactive.brand.surface.strong.default,
|
|
194
|
+
},
|
|
195
|
+
}));
|
|
196
|
+
export default SegmentedControl;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ViewProps } from 'react-native';
|
|
3
|
+
import type { FlexLayoutProps } from '../../types';
|
|
4
|
+
export interface SegmentedControlProps extends ViewProps, FlexLayoutProps {
|
|
5
|
+
/** Controlled selected option value. */
|
|
6
|
+
value?: string;
|
|
7
|
+
/** Initial selected option value for uncontrolled mode. */
|
|
8
|
+
defaultValue?: string;
|
|
9
|
+
/** Called when selected option changes. */
|
|
10
|
+
onValueChange?: (value: string) => void;
|
|
11
|
+
/** Size variant. */
|
|
12
|
+
size?: 'sm' | 'md';
|
|
13
|
+
/** Disables all options in the control. */
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
/** SegmentedControlOption children. */
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
export default SegmentedControlProps;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type SegmentedControlOptionProps from './SegmentedControlOption.props';
|
|
2
|
+
declare const SegmentedControlOption: import("react").ForwardRefExoticComponent<SegmentedControlOptionProps & {
|
|
3
|
+
states?: {
|
|
4
|
+
active?: boolean;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
};
|
|
7
|
+
} & Omit<import("react-native").PressableProps, "children"> & {
|
|
8
|
+
tabIndex?: 0 | -1 | undefined;
|
|
9
|
+
} & {
|
|
10
|
+
children?: import("react").ReactNode | (({ hovered, pressed, focused, focusVisible, disabled, }: {
|
|
11
|
+
hovered?: boolean | undefined;
|
|
12
|
+
pressed?: boolean | undefined;
|
|
13
|
+
focused?: boolean | undefined;
|
|
14
|
+
focusVisible?: boolean | undefined;
|
|
15
|
+
disabled?: boolean | undefined;
|
|
16
|
+
}) => import("react").ReactNode);
|
|
17
|
+
} & import("react").RefAttributes<unknown>>;
|
|
18
|
+
export default SegmentedControlOption;
|