@utilitywarehouse/hearth-react-native 0.28.6 → 0.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.storybook/preview.tsx +7 -4
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-lint.log +15 -18
- package/CHANGELOG.md +59 -0
- package/build/components/Combobox/Combobox.js +1 -1
- package/build/components/Modal/Modal.d.ts +1 -1
- package/build/components/Modal/Modal.js +6 -95
- package/build/components/Modal/Modal.props.d.ts +2 -31
- package/build/components/Modal/Modal.shared.types.d.ts +22 -0
- package/build/components/Modal/Modal.web.d.ts +1 -1
- package/build/components/Modal/Modal.web.js +6 -71
- package/build/components/NavModal/NavModal.d.ts +3 -0
- package/build/components/NavModal/NavModal.js +190 -0
- package/build/components/NavModal/NavModal.props.d.ts +15 -0
- package/build/components/NavModal/NavModal.props.js +1 -0
- package/build/components/NavModal/index.d.ts +2 -0
- package/build/components/NavModal/index.js +1 -0
- package/build/components/Select/Select.js +1 -1
- package/build/components/index.d.ts +2 -1
- package/build/components/index.js +2 -1
- package/docs/changelog.mdx +34 -0
- package/docs/components/AllComponents.web.tsx +709 -689
- package/package.json +5 -3
- package/src/components/Combobox/Combobox.tsx +1 -1
- package/src/components/Modal/Modal.docs.mdx +5 -122
- package/src/components/Modal/Modal.props.ts +2 -34
- package/src/components/Modal/Modal.shared.types.ts +23 -0
- package/src/components/Modal/Modal.stories.tsx +0 -1
- package/src/components/Modal/Modal.tsx +11 -174
- package/src/components/Modal/Modal.web.tsx +29 -127
- package/src/components/NavModal/NavModal.docs.mdx +178 -0
- package/src/components/NavModal/NavModal.figma.tsx +13 -0
- package/src/components/NavModal/NavModal.props.ts +23 -0
- package/src/components/NavModal/NavModal.stories.tsx +131 -0
- package/src/components/NavModal/NavModal.tsx +375 -0
- package/src/components/NavModal/index.ts +2 -0
- package/src/components/Select/Select.tsx +1 -1
- package/src/components/index.ts +3 -1
- package/build/components/SafeAreaView/SafeAreaView.d.ts +0 -5
- package/build/components/SafeAreaView/SafeAreaView.js +0 -117
- package/build/components/SafeAreaView/SafeAreaView.props.d.ts +0 -17
- package/build/components/SafeAreaView/index.d.ts +0 -2
- package/build/components/SafeAreaView/index.js +0 -1
- package/src/components/SafeAreaView/SafeAreaView.props.ts +0 -20
- package/src/components/SafeAreaView/SafeAreaView.tsx +0 -173
- package/src/components/SafeAreaView/index.ts +0 -2
- /package/build/components/{SafeAreaView/SafeAreaView.props.js → Modal/Modal.shared.types.js} +0 -0
|
@@ -1,18 +1,9 @@
|
|
|
1
1
|
import { BottomSheetScrollViewMethods, SNAP_POINT_TYPE } from '@gorhom/bottom-sheet';
|
|
2
2
|
import { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/types';
|
|
3
3
|
import { CloseMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
|
|
4
|
-
import {
|
|
4
|
+
import { useImperativeHandle, useRef } from 'react';
|
|
5
5
|
import { AccessibilityInfo, Platform, View, findNodeHandle } from 'react-native';
|
|
6
|
-
import Animated, {
|
|
7
|
-
Easing,
|
|
8
|
-
useAnimatedStyle,
|
|
9
|
-
useSharedValue,
|
|
10
|
-
withDelay,
|
|
11
|
-
withTiming,
|
|
12
|
-
} from 'react-native-reanimated';
|
|
13
6
|
import { StyleSheet } from 'react-native-unistyles';
|
|
14
|
-
import { useTheme } from '../../hooks';
|
|
15
|
-
import { hexWithOpacity } from '../../utils';
|
|
16
7
|
import { BodyText } from '../BodyText';
|
|
17
8
|
import { BottomSheetModal, BottomSheetScrollView } from '../BottomSheet';
|
|
18
9
|
import { Button } from '../Button';
|
|
@@ -38,74 +29,19 @@ const Modal = ({
|
|
|
38
29
|
closeOnSecondaryButtonPress = true,
|
|
39
30
|
loading,
|
|
40
31
|
loadingHeading = 'Loading...',
|
|
32
|
+
fullscreen = false,
|
|
41
33
|
image,
|
|
42
34
|
primaryButtonProps,
|
|
43
35
|
secondaryButtonProps,
|
|
44
36
|
closeButtonProps,
|
|
45
|
-
inNavModal = false,
|
|
46
37
|
...props
|
|
47
38
|
}: ModalProps) => {
|
|
48
39
|
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
|
49
40
|
const viewRef = useRef<View>(null);
|
|
50
41
|
const scrollViewRef = useRef<BottomSheetScrollViewMethods>(null);
|
|
51
|
-
const theme = useTheme();
|
|
52
|
-
const backgroundOpacity = useSharedValue(0);
|
|
53
|
-
const pretendContentTranslateY = useSharedValue(20);
|
|
54
|
-
|
|
55
|
-
const triggerCloseAnimation = useCallback(() => {
|
|
56
|
-
if (Platform.OS === 'android' && inNavModal) {
|
|
57
|
-
pretendContentTranslateY.value = withTiming(20, {
|
|
58
|
-
duration: 50,
|
|
59
|
-
easing: Easing.in(Easing.quad),
|
|
60
|
-
});
|
|
61
|
-
backgroundOpacity.value = withTiming(0, {
|
|
62
|
-
duration: 100,
|
|
63
|
-
easing: Easing.in(Easing.quad),
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
}, [Platform.OS, inNavModal, pretendContentTranslateY, backgroundOpacity]);
|
|
67
42
|
|
|
68
43
|
useImperativeHandle(ref, () => ({
|
|
69
44
|
...(bottomSheetModalRef.current as BottomSheetModal),
|
|
70
|
-
triggerCloseAnimation,
|
|
71
|
-
}));
|
|
72
|
-
|
|
73
|
-
// Trigger animations on render for inNavModal Android modal
|
|
74
|
-
useEffect(() => {
|
|
75
|
-
if (Platform.OS === 'android' && inNavModal) {
|
|
76
|
-
backgroundOpacity.value = withDelay(
|
|
77
|
-
300,
|
|
78
|
-
withTiming(1, {
|
|
79
|
-
duration: 200,
|
|
80
|
-
easing: Easing.out(Easing.quad),
|
|
81
|
-
})
|
|
82
|
-
);
|
|
83
|
-
pretendContentTranslateY.value = withDelay(
|
|
84
|
-
500,
|
|
85
|
-
withTiming(0, {
|
|
86
|
-
duration: 300,
|
|
87
|
-
easing: Easing.out(Easing.quad),
|
|
88
|
-
})
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
}, [inNavModal, backgroundOpacity, pretendContentTranslateY]);
|
|
92
|
-
|
|
93
|
-
const animatedBackgroundStyle = useAnimatedStyle(() => ({
|
|
94
|
-
backgroundColor: hexWithOpacity(
|
|
95
|
-
theme.components.overlay.backgroundColor,
|
|
96
|
-
backgroundOpacity.value * (theme.components.overlay.opacity / 100)
|
|
97
|
-
),
|
|
98
|
-
}));
|
|
99
|
-
|
|
100
|
-
const animatedInNavModalStyle = useAnimatedStyle(() => ({
|
|
101
|
-
backgroundColor: hexWithOpacity(
|
|
102
|
-
theme.components.overlay.backgroundColor,
|
|
103
|
-
backgroundOpacity.value * (theme.components.overlay.opacity / 100)
|
|
104
|
-
),
|
|
105
|
-
}));
|
|
106
|
-
|
|
107
|
-
const animatedPretendContentStyle = useAnimatedStyle(() => ({
|
|
108
|
-
transform: [{ translateY: pretendContentTranslateY.value }],
|
|
109
45
|
}));
|
|
110
46
|
|
|
111
47
|
const handleChange = (index: number, position: number, type: SNAP_POINT_TYPE) => {
|
|
@@ -155,6 +91,8 @@ const Modal = ({
|
|
|
155
91
|
}
|
|
156
92
|
};
|
|
157
93
|
|
|
94
|
+
const noButtons = !onPressPrimaryButton && !onPressSecondaryButton;
|
|
95
|
+
|
|
158
96
|
const content = (
|
|
159
97
|
<>
|
|
160
98
|
{loading ? (
|
|
@@ -214,49 +152,38 @@ const Modal = ({
|
|
|
214
152
|
</View>
|
|
215
153
|
) : null}
|
|
216
154
|
{children}
|
|
217
|
-
|
|
218
|
-
{
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
155
|
+
{!noButtons ? (
|
|
156
|
+
<View style={styles.footer}>
|
|
157
|
+
{onPressPrimaryButton && primaryButtonText ? (
|
|
158
|
+
<Button
|
|
159
|
+
onPress={handlePrimaryButtonPress}
|
|
160
|
+
text={primaryButtonText}
|
|
161
|
+
{...primaryButtonProps}
|
|
162
|
+
variant={(primaryButtonProps?.variant as 'solid') ?? 'solid'}
|
|
163
|
+
colorScheme={(primaryButtonProps?.colorScheme as 'highlight') ?? 'highlight'}
|
|
164
|
+
/>
|
|
165
|
+
) : null}
|
|
166
|
+
{onPressSecondaryButton && secondaryButtonText ? (
|
|
167
|
+
<Button
|
|
168
|
+
onPress={handleSecondaryButtonPress}
|
|
169
|
+
text={secondaryButtonText}
|
|
170
|
+
{...secondaryButtonProps}
|
|
171
|
+
variant={(secondaryButtonProps?.variant as 'outline') ?? 'outline'}
|
|
172
|
+
colorScheme={(secondaryButtonProps?.colorScheme as 'functional') ?? 'functional'}
|
|
173
|
+
/>
|
|
174
|
+
) : null}
|
|
175
|
+
</View>
|
|
176
|
+
) : null}
|
|
237
177
|
</View>
|
|
238
178
|
)}
|
|
239
179
|
</>
|
|
240
180
|
);
|
|
241
181
|
|
|
242
|
-
return
|
|
243
|
-
<View style={{ flex: 1, backgroundColor: theme.color.background.primary }}>
|
|
244
|
-
{Platform.OS === 'android' ? (
|
|
245
|
-
<Animated.View style={[styles.androidContainer, animatedBackgroundStyle]}>
|
|
246
|
-
<Animated.View style={[styles.pretendContent, animatedPretendContentStyle]} />
|
|
247
|
-
</Animated.View>
|
|
248
|
-
) : null}
|
|
249
|
-
<Animated.View
|
|
250
|
-
style={[styles.inNavModalContainer, Platform.OS === 'android' && animatedInNavModalStyle]}
|
|
251
|
-
>
|
|
252
|
-
<View style={styles.inNavModalContent}>{content}</View>
|
|
253
|
-
</Animated.View>
|
|
254
|
-
</View>
|
|
255
|
-
) : (
|
|
182
|
+
return (
|
|
256
183
|
<BottomSheetModal
|
|
257
184
|
ref={bottomSheetModalRef}
|
|
258
185
|
enableDynamicSizing={true}
|
|
259
|
-
snapPoints={image ? ['90%'] : props.snapPoints}
|
|
186
|
+
snapPoints={image || fullscreen ? ['90%'] : props.snapPoints}
|
|
260
187
|
showHandle={typeof loading !== 'undefined' && loading ? false : props.showHandle}
|
|
261
188
|
accessible={false}
|
|
262
189
|
{...props}
|
|
@@ -269,7 +196,7 @@ const Modal = ({
|
|
|
269
196
|
);
|
|
270
197
|
};
|
|
271
198
|
|
|
272
|
-
const styles = StyleSheet.create(
|
|
199
|
+
const styles = StyleSheet.create(theme => ({
|
|
273
200
|
container: {
|
|
274
201
|
flex: 1,
|
|
275
202
|
gap: theme.components.modal.gap,
|
|
@@ -300,31 +227,6 @@ const styles = StyleSheet.create((theme, rt) => ({
|
|
|
300
227
|
footer: {
|
|
301
228
|
gap: theme.components.modal.action.gap,
|
|
302
229
|
},
|
|
303
|
-
inNavModalContainer: {
|
|
304
|
-
flex: 1,
|
|
305
|
-
...(Platform.OS === 'ios' ? { backgroundColor: theme.components.overlay.backgroundColor } : {}),
|
|
306
|
-
},
|
|
307
|
-
inNavModalContent: {
|
|
308
|
-
flex: 1,
|
|
309
|
-
borderTopLeftRadius: theme.components.modal.borderRadius,
|
|
310
|
-
borderTopRightRadius: theme.components.modal.borderRadius,
|
|
311
|
-
backgroundColor: theme.color.surface.neutral.strong,
|
|
312
|
-
gap: theme.components.modal.gap,
|
|
313
|
-
padding: theme.components.bottomSheet.padding,
|
|
314
|
-
paddingBottom: theme.components.modal.padding + rt.insets.bottom,
|
|
315
|
-
},
|
|
316
|
-
androidContainer: {
|
|
317
|
-
height: rt.insets.top + 18,
|
|
318
|
-
paddingLeft: theme.components.bottomSheet.padding,
|
|
319
|
-
paddingRight: theme.components.bottomSheet.padding,
|
|
320
|
-
justifyContent: 'flex-end',
|
|
321
|
-
},
|
|
322
|
-
pretendContent: {
|
|
323
|
-
borderTopLeftRadius: theme.components.modal.borderRadius,
|
|
324
|
-
borderTopRightRadius: theme.components.modal.borderRadius,
|
|
325
|
-
height: 12,
|
|
326
|
-
backgroundColor: theme.components.parts.modalStack.backgroundColorCardTop,
|
|
327
|
-
},
|
|
328
230
|
}));
|
|
329
231
|
|
|
330
232
|
export default Modal;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks';
|
|
2
|
+
import StorybookLink from '../../../../../shared/storybook/StorybookLink';
|
|
3
|
+
import modalAndroidVideo from '../../../docs/assets/modal-android.mp4';
|
|
4
|
+
import modaliOSVideo from '../../../docs/assets/modal-ios.mp4';
|
|
5
|
+
import { BackToTopButton, ViewFigmaButton } from '../../../docs/components';
|
|
6
|
+
import * as Stories from './NavModal.stories';
|
|
7
|
+
|
|
8
|
+
<Meta title="Components / NavModal" />
|
|
9
|
+
|
|
10
|
+
<ViewFigmaButton url="https://www.figma.com/design/dLI9bmyMr42LV7dtFeW27J/Hearth-Patterns---Guides?node-id=6314-9103&t=oq3NaPLaAu3di6Db-4" />
|
|
11
|
+
|
|
12
|
+
<BackToTopButton />
|
|
13
|
+
|
|
14
|
+
# NavModal
|
|
15
|
+
|
|
16
|
+
The `NavModal` component is the screen-based modal layout for navigation flows. Use it when a screen is already being presented by React Navigation with `presentation: 'modal'` or `presentation: 'fullScreenModal'` and you want Hearth's modal structure, actions, and Android close animation support.
|
|
17
|
+
|
|
18
|
+
If you need a bottom-sheet modal that you present with a ref, use <StorybookLink to="components-modal">`Modal`</StorybookLink> instead.
|
|
19
|
+
|
|
20
|
+
- [Playground](#playground)
|
|
21
|
+
- [Usage](#usage)
|
|
22
|
+
- [Props](#props)
|
|
23
|
+
- [Features](#features)
|
|
24
|
+
- [Accessibility](#accessibility)
|
|
25
|
+
- [Examples](#examples)
|
|
26
|
+
|
|
27
|
+
## Playground
|
|
28
|
+
|
|
29
|
+
<Canvas of={Stories.Playground} />
|
|
30
|
+
|
|
31
|
+
<Controls of={Stories.Playground} />
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
import { useNavigation } from '@react-navigation/native';
|
|
37
|
+
import type { NavigationAction } from '@react-navigation/native';
|
|
38
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
39
|
+
import { Platform, StyleSheet, View } from 'react-native';
|
|
40
|
+
|
|
41
|
+
import {
|
|
42
|
+
BodyText,
|
|
43
|
+
Heading,
|
|
44
|
+
InlineLink,
|
|
45
|
+
NavModal,
|
|
46
|
+
type NavModalRef,
|
|
47
|
+
} from '@utilitywarehouse/hearth-react-native';
|
|
48
|
+
|
|
49
|
+
export default function ModalScreen({ onClose }: { onClose?: () => void }) {
|
|
50
|
+
const modalRef = useRef<NavModalRef>(null);
|
|
51
|
+
const navigation = useNavigation();
|
|
52
|
+
const isClosingRef = useRef(false);
|
|
53
|
+
|
|
54
|
+
const handleClose = useCallback(
|
|
55
|
+
(action?: NavigationAction) => {
|
|
56
|
+
if (Platform.OS === 'ios') {
|
|
57
|
+
if (onClose) {
|
|
58
|
+
onClose();
|
|
59
|
+
} else {
|
|
60
|
+
navigation.goBack();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (isClosingRef.current) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
isClosingRef.current = true;
|
|
71
|
+
modalRef.current?.triggerCloseAnimation?.();
|
|
72
|
+
|
|
73
|
+
setTimeout(() => {
|
|
74
|
+
if (onClose) {
|
|
75
|
+
onClose();
|
|
76
|
+
} else if (action) {
|
|
77
|
+
navigation.dispatch(action);
|
|
78
|
+
} else {
|
|
79
|
+
navigation.goBack();
|
|
80
|
+
}
|
|
81
|
+
}, 100);
|
|
82
|
+
},
|
|
83
|
+
[navigation, onClose]
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (Platform.OS === 'android') {
|
|
88
|
+
const unsubscribe = navigation.addListener('beforeRemove', e => {
|
|
89
|
+
if (!isClosingRef.current) {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
handleClose(e.data.action);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return unsubscribe;
|
|
96
|
+
}
|
|
97
|
+
}, [handleClose, navigation]);
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<NavModal
|
|
101
|
+
ref={modalRef}
|
|
102
|
+
onPressCloseButton={handleClose}
|
|
103
|
+
primaryButtonText="Action"
|
|
104
|
+
onPressPrimaryButton={handleClose}
|
|
105
|
+
secondaryButtonText="Cancel"
|
|
106
|
+
onPressSecondaryButton={handleClose}
|
|
107
|
+
>
|
|
108
|
+
<View style={styles.container}>
|
|
109
|
+
<Heading size="xl">This is a modal</Heading>
|
|
110
|
+
<BodyText>
|
|
111
|
+
<InlineLink onPress={handleClose} style={styles.link}>
|
|
112
|
+
Go to home screen
|
|
113
|
+
</InlineLink>
|
|
114
|
+
</BodyText>
|
|
115
|
+
</View>
|
|
116
|
+
</NavModal>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const styles = StyleSheet.create({
|
|
121
|
+
container: {
|
|
122
|
+
flex: 1,
|
|
123
|
+
alignItems: 'center',
|
|
124
|
+
justifyContent: 'center',
|
|
125
|
+
padding: 20,
|
|
126
|
+
},
|
|
127
|
+
link: {
|
|
128
|
+
marginTop: 15,
|
|
129
|
+
paddingVertical: 15,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Props
|
|
135
|
+
|
|
136
|
+
`NavModal` supports Hearth's modal content props plus the navigation-specific `background`, `scrollable`, and `presentation` options. Set `presentation` to the same value you pass to React Navigation so `NavModal` can match the route's sheet-style or full-screen layout. It does not take bottom-sheet props such as `snapPoints`, and it does not manage its own dismissal. Your screen or navigator owns closing behavior.
|
|
137
|
+
|
|
138
|
+
| Property | Type | Description | Default |
|
|
139
|
+
| ------------------------ | ---------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | -------------- |
|
|
140
|
+
| `heading` | `string` | Heading text shown at the top of the modal when no `image` is provided. | - |
|
|
141
|
+
| `description` | `string` | Supporting text shown below the heading when no `image` is provided. | - |
|
|
142
|
+
| `showCloseButton` | `boolean` | Whether to render the close button in the top-right corner. | `true` |
|
|
143
|
+
| `primaryButtonText` | `string` | Label for the primary action button. | - |
|
|
144
|
+
| `secondaryButtonText` | `string` | Label for the secondary action button. | - |
|
|
145
|
+
| `onPressPrimaryButton` | `() => void` | Called when the primary action button is pressed. | - |
|
|
146
|
+
| `onPressSecondaryButton` | `() => void` | Called when the secondary action button is pressed. | - |
|
|
147
|
+
| `onPressCloseButton` | `() => void` | Called when the close button is pressed. | - |
|
|
148
|
+
| `primaryButtonProps` | `Omit<ButtonWithoutChildrenProps, 'children'>` | Extra props forwarded to the primary button. | - |
|
|
149
|
+
| `secondaryButtonProps` | `Omit<ButtonWithoutChildrenProps, 'children'>` | Extra props forwarded to the secondary button. | - |
|
|
150
|
+
| `closeButtonProps` | `Omit<UnstyledIconButtonProps, 'children'>` | Extra props forwarded to the close button. | - |
|
|
151
|
+
| `loading` | `boolean` | Replaces the content with a loading state and spinner. | `false` |
|
|
152
|
+
| `loadingHeading` | `string` | Heading text shown while `loading` is true. | `'Loading...'` |
|
|
153
|
+
| `image` | `ReactNode` | Optional image or illustration shown above the text content. | - |
|
|
154
|
+
| `children` | `ReactNode` | Content rendered inside the modal body. | - |
|
|
155
|
+
| `stickyFooter` | `boolean` | Keeps action buttons pinned to the bottom instead of flowing with the content. | `true` |
|
|
156
|
+
| `background` | `'default' /\| 'brand'` | Switches between the default surface background and the brand background treatment. | `'default'` |
|
|
157
|
+
| `scrollable` | `boolean` | Wraps the content area in a `ScrollView`. Set this to `false` for custom layouts that should not scroll. | `true` |
|
|
158
|
+
| `presentation` | `'modal' \| 'fullScreenModal' \| 'transparentModal' `<br />` \| 'containedModal' \| 'containedTransparentModal'` | Matches the React Navigation screen presentation. `fullScreenModal` uses the full-screen layout; the other values use the sheet-style layout. | `'modal'` |
|
|
159
|
+
| `safeAreaViewProps` | `SafeAreaViewProps` | Extra props forwarded to the `SafeAreaView` wrapping the modal content. | - |
|
|
160
|
+
| `scrollViewProps` | `ScrollViewProps` | Extra props forwarded to the `ScrollView` wrapping the modal content when `scrollable` is true. | - |
|
|
161
|
+
|
|
162
|
+
## Accessibility
|
|
163
|
+
|
|
164
|
+
`NavModal` keeps the same heading, description, button labeling, and loading-state support as `Modal`, but it does not force accessibility focus on mount. Because this component is used on a navigation-presented screen, React Navigation and the platform own the initial screen focus behavior.
|
|
165
|
+
|
|
166
|
+
## Examples
|
|
167
|
+
|
|
168
|
+
| ios | android |
|
|
169
|
+
| ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- |
|
|
170
|
+
| <video src={modaliOSVideo} width={300} height="auto" controls loop autoPlay /> | <video src={modalAndroidVideo} width={300} height="auto" controls loop autoPlay /> |
|
|
171
|
+
|
|
172
|
+
### Brand Background
|
|
173
|
+
|
|
174
|
+
<Canvas of={Stories.BrandBackground} />
|
|
175
|
+
|
|
176
|
+
### Full-Screen Presentation
|
|
177
|
+
|
|
178
|
+
<Canvas of={Stories.FullScreenPresentation} />
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import figma from '@figma/code-connect';
|
|
2
|
+
import { NavModal } from '..';
|
|
3
|
+
|
|
4
|
+
figma.connect(
|
|
5
|
+
NavModal,
|
|
6
|
+
'https://www.figma.com/design/dLI9bmyMr42LV7dtFeW27J/Hearth-Patterns---Guides?node-id=6314-9103&t=oq3NaPLaAu3di6Db-4',
|
|
7
|
+
{
|
|
8
|
+
props: {},
|
|
9
|
+
example: props => {
|
|
10
|
+
return <NavModal>{/* Your content here */}</NavModal>;
|
|
11
|
+
},
|
|
12
|
+
}
|
|
13
|
+
);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Ref } from 'react';
|
|
2
|
+
import { SafeAreaViewProps } from 'react-native-safe-area-context';
|
|
3
|
+
import { ModalCommonProps } from '../Modal/Modal.shared.types';
|
|
4
|
+
|
|
5
|
+
export interface NavModalRef {
|
|
6
|
+
triggerCloseAnimation?: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface NavModalProps extends ModalCommonProps {
|
|
10
|
+
ref?: Ref<NavModalRef>;
|
|
11
|
+
background?: 'default' | 'brand';
|
|
12
|
+
scrollable?: boolean;
|
|
13
|
+
presentation?:
|
|
14
|
+
| 'fullScreenModal'
|
|
15
|
+
| 'modal'
|
|
16
|
+
| 'transparentModal'
|
|
17
|
+
| 'containedModal'
|
|
18
|
+
| 'containedTransparentModal';
|
|
19
|
+
safeAreaViewProps?: Omit<SafeAreaViewProps, 'children'>;
|
|
20
|
+
scrollViewProps?: Omit<SafeAreaViewProps, 'children'>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default NavModalProps;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react-native';
|
|
2
|
+
import { Platform, View } from 'react-native';
|
|
3
|
+
import { BodyText } from '../BodyText';
|
|
4
|
+
import { Box } from '../Box';
|
|
5
|
+
import NavModal from './NavModal';
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
title: 'Stories / NavModal',
|
|
9
|
+
component: NavModal,
|
|
10
|
+
parameters: {
|
|
11
|
+
layout: 'fullscreen',
|
|
12
|
+
noScroll: true,
|
|
13
|
+
},
|
|
14
|
+
argTypes: {
|
|
15
|
+
heading: {
|
|
16
|
+
control: 'text',
|
|
17
|
+
description: 'The heading text to be displayed.',
|
|
18
|
+
},
|
|
19
|
+
description: {
|
|
20
|
+
control: 'text',
|
|
21
|
+
description: 'The description text to be displayed.',
|
|
22
|
+
},
|
|
23
|
+
showCloseButton: {
|
|
24
|
+
control: 'boolean',
|
|
25
|
+
description: 'Whether to show the close button.',
|
|
26
|
+
},
|
|
27
|
+
primaryButtonText: {
|
|
28
|
+
control: 'text',
|
|
29
|
+
description: 'Text for the primary button.',
|
|
30
|
+
},
|
|
31
|
+
secondaryButtonText: {
|
|
32
|
+
control: 'text',
|
|
33
|
+
description: 'Text for the secondary button.',
|
|
34
|
+
},
|
|
35
|
+
loading: {
|
|
36
|
+
control: 'boolean',
|
|
37
|
+
description: 'Whether the modal is in a loading state.',
|
|
38
|
+
},
|
|
39
|
+
loadingHeading: {
|
|
40
|
+
control: 'text',
|
|
41
|
+
description: 'The heading text to be displayed when loading is true.',
|
|
42
|
+
},
|
|
43
|
+
background: {
|
|
44
|
+
control: 'radio',
|
|
45
|
+
options: ['default', 'brand'],
|
|
46
|
+
description: 'Sets the modal background.',
|
|
47
|
+
},
|
|
48
|
+
presentation: {
|
|
49
|
+
control: 'radio',
|
|
50
|
+
options: [
|
|
51
|
+
'modal',
|
|
52
|
+
'fullScreenModal',
|
|
53
|
+
'transparentModal',
|
|
54
|
+
'containedModal',
|
|
55
|
+
'containedTransparentModal',
|
|
56
|
+
],
|
|
57
|
+
description: 'Matches the React Navigation presentation style for the screen.',
|
|
58
|
+
},
|
|
59
|
+
scrollable: {
|
|
60
|
+
control: 'boolean',
|
|
61
|
+
description: 'Whether the modal content should be wrapped in a ScrollView.',
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
actions: {
|
|
65
|
+
onPressPrimaryButton: { action: () => null },
|
|
66
|
+
onPressSecondaryButton: { action: () => null },
|
|
67
|
+
onPressCloseButton: { action: () => null },
|
|
68
|
+
},
|
|
69
|
+
args: {
|
|
70
|
+
heading: 'NavModal Heading',
|
|
71
|
+
description: 'This is a navigation modal description',
|
|
72
|
+
showCloseButton: true,
|
|
73
|
+
primaryButtonText: 'Primary',
|
|
74
|
+
secondaryButtonText: 'Cancel',
|
|
75
|
+
loading: false,
|
|
76
|
+
background: 'default',
|
|
77
|
+
scrollable: true,
|
|
78
|
+
presentation: 'modal',
|
|
79
|
+
onPressCloseButton: () => null,
|
|
80
|
+
onPressPrimaryButton: () => null,
|
|
81
|
+
onPressSecondaryButton: () => null,
|
|
82
|
+
},
|
|
83
|
+
} satisfies Meta<typeof NavModal>;
|
|
84
|
+
|
|
85
|
+
export default meta;
|
|
86
|
+
type Story = StoryObj<typeof meta>;
|
|
87
|
+
|
|
88
|
+
export const Playground: Story = {
|
|
89
|
+
render: (args: StoryObj<typeof meta.args>) => (
|
|
90
|
+
<View style={Platform.OS === 'web' ? { width: 400, height: 720 } : { flex: 1 }}>
|
|
91
|
+
<NavModal {...args}>
|
|
92
|
+
<Box gap="200">
|
|
93
|
+
<BodyText>This is a navigation modal with content.</BodyText>
|
|
94
|
+
<BodyText>Use it for React Navigation modal screens instead of bottom sheets.</BodyText>
|
|
95
|
+
</Box>
|
|
96
|
+
</NavModal>
|
|
97
|
+
</View>
|
|
98
|
+
),
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const BrandBackground: Story = {
|
|
102
|
+
args: {
|
|
103
|
+
background: 'brand',
|
|
104
|
+
},
|
|
105
|
+
render: (args: StoryObj<typeof meta.args>) => (
|
|
106
|
+
<View style={Platform.OS === 'web' ? { width: 400, height: 720 } : { flex: 1 }}>
|
|
107
|
+
<NavModal {...args}>
|
|
108
|
+
<Box gap="200">
|
|
109
|
+
<BodyText inverted>Brand background content stays readable with inverted text.</BodyText>
|
|
110
|
+
<BodyText inverted>Buttons and the close icon also invert automatically.</BodyText>
|
|
111
|
+
</Box>
|
|
112
|
+
</NavModal>
|
|
113
|
+
</View>
|
|
114
|
+
),
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const FullScreenPresentation: Story = {
|
|
118
|
+
args: {
|
|
119
|
+
presentation: 'fullScreenModal',
|
|
120
|
+
},
|
|
121
|
+
render: (args: StoryObj<typeof meta.args>) => (
|
|
122
|
+
<View style={Platform.OS === 'web' ? { width: 400, height: 720 } : { flex: 1 }}>
|
|
123
|
+
<NavModal {...args}>
|
|
124
|
+
<Box gap="200">
|
|
125
|
+
<BodyText>This uses the full-screen navigation modal presentation.</BodyText>
|
|
126
|
+
<BodyText>The content switches away from the sheet-style card treatment.</BodyText>
|
|
127
|
+
</Box>
|
|
128
|
+
</NavModal>
|
|
129
|
+
</View>
|
|
130
|
+
),
|
|
131
|
+
};
|