@utilitywarehouse/hearth-react-native 0.30.4 → 0.31.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 +12 -15
- package/CHANGELOG.md +149 -0
- package/build/components/Badge/Badge.js +2 -2
- package/build/components/Badge/Badge.props.d.ts +1 -0
- package/build/components/Badge/BadgeText.d.ts +1 -1
- package/build/components/Badge/BadgeText.js +2 -2
- package/build/components/Container/Container.props.d.ts +2 -2
- package/build/components/ExpandableCard/ExpandableCard.d.ts +1 -1
- package/build/components/ExpandableCard/ExpandableCard.js +13 -2
- package/build/components/ExpandableCard/ExpandableCard.props.d.ts +43 -23
- package/build/components/ExpandableCard/ExpandableCardText.js +1 -1
- package/build/components/ExpandableCard/ExpandableCardTrigger.d.ts +3 -3
- package/build/components/ExpandableCard/ExpandableCardTrigger.props.d.ts +31 -6
- package/build/components/ExpandableCard/ExpandableCardTriggerRoot.d.ts +1 -1
- package/build/components/ExpandableCard/ExpandableCardTriggerRoot.js +13 -2
- package/build/components/Flex/Flex.props.d.ts +2 -2
- package/build/components/FormField/FormField.d.ts +5 -5
- package/build/components/FormField/FormField.js +3 -2
- package/build/components/Modal/Modal.d.ts +1 -1
- package/build/components/Modal/Modal.js +33 -39
- package/build/components/Modal/Modal.props.d.ts +8 -3
- package/build/components/Modal/Modal.shared.types.d.ts +19 -4
- package/build/components/Modal/Modal.web.d.ts +1 -1
- package/build/components/Modal/Modal.web.js +6 -3
- package/build/components/NavModal/NavModal.d.ts +1 -1
- package/build/components/NavModal/NavModal.js +10 -7
- package/build/components/NavModal/NavModal.props.d.ts +4 -3
- package/build/components/Textarea/Textarea.d.ts +1 -1
- package/build/components/Textarea/Textarea.js +64 -5
- package/build/components/Textarea/Textarea.props.d.ts +10 -0
- package/build/components/Textarea/TextareaRoot.js +4 -1
- package/docs/changelog.mdx +21 -0
- package/package.json +1 -1
- package/src/components/Badge/Badge.props.ts +1 -0
- package/src/components/Badge/Badge.tsx +6 -1
- package/src/components/Badge/BadgeText.tsx +8 -2
- package/src/components/Container/Container.props.ts +10 -1
- package/src/components/ExpandableCard/ExpandableCard.docs.mdx +89 -37
- package/src/components/ExpandableCard/ExpandableCard.props.ts +51 -27
- package/src/components/ExpandableCard/ExpandableCard.stories.tsx +67 -17
- package/src/components/ExpandableCard/ExpandableCard.tsx +15 -7
- package/src/components/ExpandableCard/ExpandableCardText.tsx +1 -1
- package/src/components/ExpandableCard/ExpandableCardTrigger.props.ts +37 -7
- package/src/components/ExpandableCard/ExpandableCardTriggerRoot.tsx +36 -2
- package/src/components/Flex/Flex.props.ts +16 -2
- package/src/components/FormField/FormField.tsx +2 -1
- package/src/components/List/List.stories.tsx +35 -0
- package/src/components/Modal/Modal.docs.mdx +52 -1
- package/src/components/Modal/Modal.props.ts +21 -6
- package/src/components/Modal/Modal.shared.types.ts +23 -4
- package/src/components/Modal/Modal.stories.tsx +165 -1
- package/src/components/Modal/Modal.tsx +101 -81
- package/src/components/Modal/Modal.web.tsx +29 -23
- package/src/components/NavModal/NavModal.docs.mdx +29 -0
- package/src/components/NavModal/NavModal.props.ts +11 -3
- package/src/components/NavModal/NavModal.stories.tsx +29 -0
- package/src/components/NavModal/NavModal.tsx +39 -33
- package/src/components/Textarea/Textarea.docs.mdx +33 -1
- package/src/components/Textarea/Textarea.props.ts +11 -2
- package/src/components/Textarea/Textarea.stories.tsx +21 -1
- package/src/components/Textarea/Textarea.tsx +107 -3
- package/src/components/Textarea/TextareaRoot.tsx +6 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Canvas, Controls, Meta, Story } from '@storybook/addon-docs/blocks';
|
|
2
|
-
import { BodyText, BottomSheetModal, Box, Button, Center, Heading, Modal } from '../../';
|
|
2
|
+
import { BodyText, BottomSheetModal, Box, Button, Center, Flex, Heading, Modal } from '../../';
|
|
3
3
|
import StorybookLink from '../../../../../shared/storybook/StorybookLink';
|
|
4
4
|
import { BackToTopButton, UsageWrap, ViewFigmaButton } from '../../../docs/components';
|
|
5
5
|
import * as Stories from './Modal.stories';
|
|
@@ -28,6 +28,7 @@ If you need a modal layout inside a React Navigation modal screen, use <Storyboo
|
|
|
28
28
|
- [Modal with Image](#modal-with-image)
|
|
29
29
|
- [Fullscreen Modal](#fullscreen-modal)
|
|
30
30
|
- [Modal with Custom Content](#modal-with-custom-content)
|
|
31
|
+
- [Sticky Custom Footer](#sticky-custom-footer)
|
|
31
32
|
- [Loading State](#loading-state)
|
|
32
33
|
- [Without Close Button](#without-close-button)
|
|
33
34
|
- [Single Action Modal](#single-action-modal)
|
|
@@ -107,9 +108,13 @@ The Modal component extends the `BottomSheetModal` component and accepts all of
|
|
|
107
108
|
| `children` | `ReactNode` | Custom content to display in the modal body | - |
|
|
108
109
|
| `primaryButtonProps` | `Omit<ButtonWithoutChildrenProps, 'children'>` | Additional props to pass to the primary button (colorScheme defaults to 'highlight', variant to 'solid') | - |
|
|
109
110
|
| `secondaryButtonProps` | `Omit<ButtonWithoutChildrenProps, 'children'>` | Additional props to pass to the secondary button (colorScheme defaults to 'functional', variant to 'outline') | - |
|
|
111
|
+
| `footer` | `ReactNode` | Custom footer content that replaces the built-in action buttons | - |
|
|
112
|
+
| `footerStyle` | `StyleProp<ViewStyle>` | Styles applied to the footer container, useful for sticky footer shadows or custom spacing | - |
|
|
110
113
|
| `closeButtonProps` | `Omit<UnstyledIconButtonProps, 'children'>` | Additional props to pass to the close button | - |
|
|
111
114
|
| `fullscreen` | `boolean` | Whether the modal should take up the full screen height | `false` |
|
|
112
115
|
|
|
116
|
+
When `footer` is provided, the primary and secondary button props are not available. Build your footer actions directly inside the custom footer content instead.
|
|
117
|
+
|
|
113
118
|
\* use this to detect if the modal has been opened or closed, index 0 indicates open state and -1 indicates closed state
|
|
114
119
|
|
|
115
120
|
### `ModalImage` Props
|
|
@@ -371,6 +376,52 @@ const CustomContentModal = () => {
|
|
|
371
376
|
};
|
|
372
377
|
```
|
|
373
378
|
|
|
379
|
+
### Sticky Custom Footer
|
|
380
|
+
|
|
381
|
+
Replace the built-in buttons with a custom sticky footer when you need custom layouts or button arrangements:
|
|
382
|
+
|
|
383
|
+
<Canvas of={Stories.StickyCustomFooter} />
|
|
384
|
+
|
|
385
|
+
```tsx
|
|
386
|
+
const StickyCustomFooterModal = () => {
|
|
387
|
+
const modalRef = useRef<BottomSheetModal>(null);
|
|
388
|
+
|
|
389
|
+
return (
|
|
390
|
+
<>
|
|
391
|
+
<Button onPress={() => modalRef.current?.present()}>Show Custom Footer Modal</Button>
|
|
392
|
+
|
|
393
|
+
<Modal
|
|
394
|
+
ref={modalRef}
|
|
395
|
+
heading="Update billing details"
|
|
396
|
+
description="Use a custom sticky footer when you need horizontal actions or extra decoration."
|
|
397
|
+
footer={
|
|
398
|
+
<Flex direction="row" spacing="md" pt="250">
|
|
399
|
+
<Button
|
|
400
|
+
variant="outline"
|
|
401
|
+
colorScheme="functional"
|
|
402
|
+
onPress={() => modalRef.current?.dismiss()}
|
|
403
|
+
style={{ flex: 1 }}
|
|
404
|
+
>
|
|
405
|
+
Cancel
|
|
406
|
+
</Button>
|
|
407
|
+
<Button onPress={() => modalRef.current?.dismiss()} style={{ flex: 1 }}>
|
|
408
|
+
Save changes
|
|
409
|
+
</Button>
|
|
410
|
+
</Flex>
|
|
411
|
+
}
|
|
412
|
+
footerStyle={{
|
|
413
|
+
boxShadow: '0px -6px 12px rgba(16, 24, 40, 0.12)',
|
|
414
|
+
}}
|
|
415
|
+
>
|
|
416
|
+
<Box gap="200">
|
|
417
|
+
<BodyText>Review the changes before saving.</BodyText>
|
|
418
|
+
</Box>
|
|
419
|
+
</Modal>
|
|
420
|
+
</>
|
|
421
|
+
);
|
|
422
|
+
};
|
|
423
|
+
```
|
|
424
|
+
|
|
374
425
|
### Loading State
|
|
375
426
|
|
|
376
427
|
Show a loading spinner while processing:
|
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
import { BottomSheetProps } from '../BottomSheet';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ModalButtonFooterProps,
|
|
4
|
+
ModalCommonBaseProps,
|
|
5
|
+
ModalCustomFooterProps,
|
|
6
|
+
} from './Modal.shared.types';
|
|
3
7
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
type ModalBaseProps = Omit<BottomSheetProps, 'children'> &
|
|
9
|
+
ModalCommonBaseProps & {
|
|
10
|
+
fullscreen?: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type ModalProps =
|
|
14
|
+
| (ModalBaseProps &
|
|
15
|
+
ModalButtonFooterProps & {
|
|
16
|
+
closeOnPrimaryButtonPress?: boolean;
|
|
17
|
+
closeOnSecondaryButtonPress?: boolean;
|
|
18
|
+
})
|
|
19
|
+
| (ModalBaseProps &
|
|
20
|
+
ModalCustomFooterProps & {
|
|
21
|
+
closeOnPrimaryButtonPress?: never;
|
|
22
|
+
closeOnSecondaryButtonPress?: never;
|
|
23
|
+
});
|
|
9
24
|
|
|
10
25
|
export default ModalProps;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
|
-
import { ViewProps } from 'react-native';
|
|
2
|
+
import { StyleProp, ViewProps, ViewStyle } from 'react-native';
|
|
3
3
|
import { ButtonWithoutChildrenProps } from '../Button/Button.props';
|
|
4
4
|
import { UnstyledIconButtonProps } from '../UnstyledIconButton';
|
|
5
5
|
|
|
6
|
-
export interface
|
|
6
|
+
export interface ModalCommonBaseProps {
|
|
7
7
|
loading?: boolean;
|
|
8
8
|
image?: ReactNode;
|
|
9
9
|
showCloseButton?: boolean;
|
|
@@ -13,12 +13,31 @@ export interface ModalCommonProps {
|
|
|
13
13
|
description?: string;
|
|
14
14
|
stickyFooter?: boolean;
|
|
15
15
|
children?: ViewProps['children'];
|
|
16
|
+
onPressCloseButton?: () => void;
|
|
17
|
+
closeButtonProps?: Omit<UnstyledIconButtonProps, 'children'>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ModalButtonFooterProps {
|
|
16
21
|
onPressPrimaryButton?: () => void;
|
|
17
22
|
primaryButtonText?: string;
|
|
18
23
|
onPressSecondaryButton?: () => void;
|
|
19
24
|
secondaryButtonText?: string;
|
|
20
|
-
onPressCloseButton?: () => void;
|
|
21
25
|
primaryButtonProps?: Omit<ButtonWithoutChildrenProps, 'children'>;
|
|
22
26
|
secondaryButtonProps?: Omit<ButtonWithoutChildrenProps, 'children'>;
|
|
23
|
-
|
|
27
|
+
footer?: never;
|
|
28
|
+
footerStyle?: StyleProp<ViewStyle>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ModalCustomFooterProps {
|
|
32
|
+
footer: ReactNode;
|
|
33
|
+
footerStyle?: StyleProp<ViewStyle>;
|
|
34
|
+
onPressPrimaryButton?: never;
|
|
35
|
+
primaryButtonText?: never;
|
|
36
|
+
onPressSecondaryButton?: never;
|
|
37
|
+
secondaryButtonText?: never;
|
|
38
|
+
primaryButtonProps?: never;
|
|
39
|
+
secondaryButtonProps?: never;
|
|
24
40
|
}
|
|
41
|
+
|
|
42
|
+
export type ModalCommonProps = ModalCommonBaseProps &
|
|
43
|
+
(ModalButtonFooterProps | ModalCustomFooterProps);
|
|
@@ -5,9 +5,10 @@ import { Modal, ModalImage } from '.';
|
|
|
5
5
|
import pigs from '../../../docs/assets/pigs.png';
|
|
6
6
|
import { ViewWrap } from '../../../docs/components';
|
|
7
7
|
import { BodyText } from '../BodyText';
|
|
8
|
-
import { BottomSheetModal } from '../BottomSheet';
|
|
8
|
+
import { BottomSheetModal, BottomSheetModalProvider } from '../BottomSheet';
|
|
9
9
|
import { Box } from '../Box';
|
|
10
10
|
import { Button } from '../Button';
|
|
11
|
+
import { Flex } from '../Flex';
|
|
11
12
|
|
|
12
13
|
const meta = {
|
|
13
14
|
title: 'Stories / Modal',
|
|
@@ -164,6 +165,56 @@ export const WithCustomContent = () => {
|
|
|
164
165
|
);
|
|
165
166
|
};
|
|
166
167
|
|
|
168
|
+
export const StickyCustomFooter = () => {
|
|
169
|
+
const modalRef = useRef<BottomSheetModal>(null);
|
|
170
|
+
|
|
171
|
+
const openModal = () => {
|
|
172
|
+
modalRef.current?.present();
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const closeModal = () => {
|
|
176
|
+
modalRef.current?.dismiss();
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<View style={Platform.OS === 'web' ? { width: 400, height: 400 } : {}}>
|
|
181
|
+
<ViewWrap>
|
|
182
|
+
<Button onPress={openModal}>Open Modal</Button>
|
|
183
|
+
|
|
184
|
+
<Modal
|
|
185
|
+
ref={modalRef}
|
|
186
|
+
heading="Update billing details"
|
|
187
|
+
description="Review the changes below, then save or discard them using a custom sticky footer."
|
|
188
|
+
onPressCloseButton={closeModal}
|
|
189
|
+
footer={
|
|
190
|
+
<Flex direction="row" spacing="md" pt="250">
|
|
191
|
+
<Button
|
|
192
|
+
variant="outline"
|
|
193
|
+
colorScheme="functional"
|
|
194
|
+
onPress={closeModal}
|
|
195
|
+
style={{ flex: 1 }}
|
|
196
|
+
>
|
|
197
|
+
Cancel
|
|
198
|
+
</Button>
|
|
199
|
+
<Button onPress={closeModal} style={{ flex: 1 }}>
|
|
200
|
+
Save changes
|
|
201
|
+
</Button>
|
|
202
|
+
</Flex>
|
|
203
|
+
}
|
|
204
|
+
footerStyle={{
|
|
205
|
+
boxShadow: '0px -6px 12px rgba(16, 24, 40, 0.12)',
|
|
206
|
+
}}
|
|
207
|
+
>
|
|
208
|
+
<Box gap="200">
|
|
209
|
+
<BodyText>Billing address: 14 Park Street, Bristol</BodyText>
|
|
210
|
+
<BodyText>Preferred payment day: 15th of each month</BodyText>
|
|
211
|
+
</Box>
|
|
212
|
+
</Modal>
|
|
213
|
+
</ViewWrap>
|
|
214
|
+
</View>
|
|
215
|
+
);
|
|
216
|
+
};
|
|
217
|
+
|
|
167
218
|
export const Loading = () => {
|
|
168
219
|
const modalRef = useRef<BottomSheetModal>(null);
|
|
169
220
|
|
|
@@ -239,3 +290,116 @@ export const FullscreenModal: Story = {
|
|
|
239
290
|
);
|
|
240
291
|
},
|
|
241
292
|
};
|
|
293
|
+
|
|
294
|
+
export const NoStickyFooter: Story = {
|
|
295
|
+
render: () => {
|
|
296
|
+
const modalRef = useRef<BottomSheetModal>(null);
|
|
297
|
+
|
|
298
|
+
const openModal = () => {
|
|
299
|
+
modalRef.current?.present();
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const closeModal = () => {
|
|
303
|
+
modalRef.current?.dismiss();
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
return (
|
|
307
|
+
<View style={Platform.OS === 'web' ? { width: 400, height: 400 } : {}}>
|
|
308
|
+
<ViewWrap>
|
|
309
|
+
<Button onPress={openModal}>Open Modal</Button>
|
|
310
|
+
|
|
311
|
+
<Modal
|
|
312
|
+
ref={modalRef}
|
|
313
|
+
heading="Modal Heading"
|
|
314
|
+
description="This is a modal description without a sticky footer."
|
|
315
|
+
onPressCloseButton={closeModal}
|
|
316
|
+
primaryButtonText="Primary"
|
|
317
|
+
onPressPrimaryButton={closeModal}
|
|
318
|
+
secondaryButtonText="Cancel"
|
|
319
|
+
onPressSecondaryButton={closeModal}
|
|
320
|
+
stickyFooter={false}
|
|
321
|
+
>
|
|
322
|
+
<Box gap="200">
|
|
323
|
+
<BodyText>This is a modal with content.</BodyText>
|
|
324
|
+
<BodyText>You can swipe it up and down to close.</BodyText>
|
|
325
|
+
</Box>
|
|
326
|
+
</Modal>
|
|
327
|
+
</ViewWrap>
|
|
328
|
+
</View>
|
|
329
|
+
);
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
export const NoFooter: Story = {
|
|
334
|
+
render: () => {
|
|
335
|
+
const modalRef = useRef<BottomSheetModal>(null);
|
|
336
|
+
|
|
337
|
+
const openModal = () => {
|
|
338
|
+
modalRef.current?.present();
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const closeModal = () => {
|
|
342
|
+
modalRef.current?.dismiss();
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
return (
|
|
346
|
+
<View style={Platform.OS === 'web' ? { width: 400, height: 400 } : {}}>
|
|
347
|
+
<ViewWrap>
|
|
348
|
+
<Button onPress={openModal}>Open Modal</Button>
|
|
349
|
+
|
|
350
|
+
<Modal
|
|
351
|
+
ref={modalRef}
|
|
352
|
+
heading="Modal Heading"
|
|
353
|
+
description="This is a modal description without a footer."
|
|
354
|
+
onPressCloseButton={closeModal}
|
|
355
|
+
>
|
|
356
|
+
<Box gap="200">
|
|
357
|
+
<BodyText>This is a modal with content.</BodyText>
|
|
358
|
+
<BodyText>You can swipe it up and down to close.</BodyText>
|
|
359
|
+
</Box>
|
|
360
|
+
</Modal>
|
|
361
|
+
</ViewWrap>
|
|
362
|
+
</View>
|
|
363
|
+
);
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
export const NoSafeArea: Story = {
|
|
368
|
+
render: () => {
|
|
369
|
+
const modalRef = useRef<BottomSheetModal>(null);
|
|
370
|
+
|
|
371
|
+
const openModal = () => {
|
|
372
|
+
modalRef.current?.present();
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const closeModal = () => {
|
|
376
|
+
modalRef.current?.dismiss();
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
return (
|
|
380
|
+
<BottomSheetModalProvider useSafeAreaInsets={false}>
|
|
381
|
+
<View style={Platform.OS === 'web' ? { width: 400, height: 400 } : {}}>
|
|
382
|
+
<ViewWrap>
|
|
383
|
+
<Button onPress={openModal}>Open Modal</Button>
|
|
384
|
+
|
|
385
|
+
<Modal
|
|
386
|
+
ref={modalRef}
|
|
387
|
+
heading="Modal Heading"
|
|
388
|
+
description="This is a modal description without safe area insets."
|
|
389
|
+
onPressCloseButton={closeModal}
|
|
390
|
+
primaryButtonText="Primary"
|
|
391
|
+
onPressPrimaryButton={closeModal}
|
|
392
|
+
secondaryButtonText="Cancel"
|
|
393
|
+
onPressSecondaryButton={closeModal}
|
|
394
|
+
>
|
|
395
|
+
<Box gap="200">
|
|
396
|
+
<BodyText>This is a modal with content.</BodyText>
|
|
397
|
+
<BodyText>You can swipe it up and down to close.</BodyText>
|
|
398
|
+
</Box>
|
|
399
|
+
</Modal>
|
|
400
|
+
</ViewWrap>
|
|
401
|
+
</View>
|
|
402
|
+
</BottomSheetModalProvider>
|
|
403
|
+
);
|
|
404
|
+
},
|
|
405
|
+
};
|
|
@@ -6,9 +6,10 @@ import {
|
|
|
6
6
|
} from '@gorhom/bottom-sheet';
|
|
7
7
|
import { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/types';
|
|
8
8
|
import { CloseMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
|
|
9
|
-
import { useCallback, useImperativeHandle, useMemo, useRef } from 'react';
|
|
10
|
-
import { AccessibilityInfo, Platform, View, findNodeHandle } from 'react-native';
|
|
9
|
+
import { useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
10
|
+
import { AccessibilityInfo, LayoutChangeEvent, Platform, View, findNodeHandle } from 'react-native';
|
|
11
11
|
import { StyleSheet } from 'react-native-unistyles';
|
|
12
|
+
import { useTheme } from '../../hooks';
|
|
12
13
|
import { BodyText } from '../BodyText';
|
|
13
14
|
import { BottomSheetModal, BottomSheetScrollView } from '../BottomSheet';
|
|
14
15
|
import { useBottomSheetContext } from '../BottomSheet/BottomSheet.context';
|
|
@@ -38,15 +39,19 @@ const Modal = ({
|
|
|
38
39
|
loadingDescription,
|
|
39
40
|
fullscreen = false,
|
|
40
41
|
image,
|
|
42
|
+
footer,
|
|
43
|
+
footerStyle,
|
|
41
44
|
primaryButtonProps,
|
|
42
45
|
secondaryButtonProps,
|
|
43
46
|
closeButtonProps,
|
|
44
47
|
stickyFooter = true,
|
|
45
48
|
...props
|
|
46
49
|
}: ModalProps) => {
|
|
50
|
+
const theme = useTheme();
|
|
47
51
|
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
|
48
52
|
const viewRef = useRef<View>(null);
|
|
49
53
|
const scrollViewRef = useRef<BottomSheetScrollViewMethods>(null);
|
|
54
|
+
const [stickyFooterHeight, setStickyFooterHeight] = useState(0);
|
|
50
55
|
const { useSafeAreaInsets } = useBottomSheetContext();
|
|
51
56
|
|
|
52
57
|
useImperativeHandle(ref, () => ({
|
|
@@ -100,45 +105,57 @@ const Modal = ({
|
|
|
100
105
|
}
|
|
101
106
|
}, [closeOnSecondaryButtonPress, onPressSecondaryButton]);
|
|
102
107
|
|
|
103
|
-
const
|
|
108
|
+
const handleStickyFooterLayout = useCallback((event: LayoutChangeEvent) => {
|
|
109
|
+
const nextHeight = Math.ceil(event.nativeEvent.layout.height);
|
|
110
|
+
|
|
111
|
+
setStickyFooterHeight(currentHeight =>
|
|
112
|
+
currentHeight === nextHeight ? currentHeight : nextHeight
|
|
113
|
+
);
|
|
114
|
+
}, []);
|
|
115
|
+
|
|
116
|
+
const hasPrimaryButton = !!(onPressPrimaryButton && primaryButtonText);
|
|
117
|
+
const hasSecondaryButton = !!(onPressSecondaryButton && secondaryButtonText);
|
|
118
|
+
const hasFooter = !!footer || hasPrimaryButton || hasSecondaryButton;
|
|
119
|
+
const shouldShowFooter = !loading && hasFooter;
|
|
104
120
|
|
|
105
121
|
styles.useVariants({
|
|
106
122
|
loading,
|
|
107
|
-
|
|
108
|
-
noButtons,
|
|
123
|
+
noButtons: !shouldShowFooter,
|
|
109
124
|
stickyFooter,
|
|
110
125
|
showHandle: props.showHandle,
|
|
111
126
|
useSafeAreaInsets,
|
|
112
127
|
});
|
|
113
128
|
|
|
114
|
-
const
|
|
115
|
-
() =>
|
|
116
|
-
|
|
117
|
-
{
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
129
|
+
const footerContent = useMemo(
|
|
130
|
+
() =>
|
|
131
|
+
footer ?? (
|
|
132
|
+
<View style={styles.footer}>
|
|
133
|
+
{hasPrimaryButton ? (
|
|
134
|
+
<Button
|
|
135
|
+
onPress={handlePrimaryButtonPress}
|
|
136
|
+
text={primaryButtonText}
|
|
137
|
+
{...primaryButtonProps}
|
|
138
|
+
variant={(primaryButtonProps?.variant as 'solid') ?? 'solid'}
|
|
139
|
+
colorScheme={(primaryButtonProps?.colorScheme as 'highlight') ?? 'highlight'}
|
|
140
|
+
/>
|
|
141
|
+
) : null}
|
|
142
|
+
{hasSecondaryButton ? (
|
|
143
|
+
<Button
|
|
144
|
+
onPress={handleSecondaryButtonPress}
|
|
145
|
+
text={secondaryButtonText}
|
|
146
|
+
{...secondaryButtonProps}
|
|
147
|
+
variant={(secondaryButtonProps?.variant as 'outline') ?? 'outline'}
|
|
148
|
+
colorScheme={(secondaryButtonProps?.colorScheme as 'functional') ?? 'functional'}
|
|
149
|
+
/>
|
|
150
|
+
) : null}
|
|
151
|
+
</View>
|
|
152
|
+
),
|
|
137
153
|
[
|
|
154
|
+
footer,
|
|
138
155
|
handlePrimaryButtonPress,
|
|
139
156
|
handleSecondaryButtonPress,
|
|
140
|
-
|
|
141
|
-
|
|
157
|
+
hasPrimaryButton,
|
|
158
|
+
hasSecondaryButton,
|
|
142
159
|
primaryButtonProps,
|
|
143
160
|
primaryButtonText,
|
|
144
161
|
secondaryButtonProps,
|
|
@@ -209,38 +226,64 @@ const Modal = ({
|
|
|
209
226
|
</View>
|
|
210
227
|
) : null}
|
|
211
228
|
{children}
|
|
212
|
-
{!stickyFooter &&
|
|
229
|
+
{!stickyFooter && shouldShowFooter ? (
|
|
230
|
+
<View style={footerStyle}>{footerContent}</View>
|
|
231
|
+
) : null}
|
|
213
232
|
</View>
|
|
214
233
|
)}
|
|
215
234
|
</>
|
|
216
235
|
);
|
|
217
236
|
|
|
218
237
|
const renderFooter = useCallback(
|
|
219
|
-
(
|
|
220
|
-
<BottomSheetFooter {...
|
|
221
|
-
<View style={styles.footerWrap}>
|
|
238
|
+
(bottomSheetFooterProps: BottomSheetFooterProps) => (
|
|
239
|
+
<BottomSheetFooter {...bottomSheetFooterProps}>
|
|
240
|
+
<View onLayout={handleStickyFooterLayout} style={[styles.footerWrap, footerStyle]}>
|
|
241
|
+
{footerContent}
|
|
242
|
+
</View>
|
|
222
243
|
</BottomSheetFooter>
|
|
223
244
|
),
|
|
224
|
-
[
|
|
245
|
+
[footerContent, footerStyle, handleStickyFooterLayout]
|
|
225
246
|
);
|
|
226
247
|
|
|
227
248
|
return (
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
249
|
+
<>
|
|
250
|
+
{stickyFooter && shouldShowFooter && stickyFooterHeight === 0 ? (
|
|
251
|
+
<View
|
|
252
|
+
accessible={false}
|
|
253
|
+
importantForAccessibility="no-hide-descendants"
|
|
254
|
+
pointerEvents="none"
|
|
255
|
+
style={styles.footerMeasurementContainer}
|
|
256
|
+
>
|
|
257
|
+
<View onLayout={handleStickyFooterLayout} style={[styles.footerWrap, footerStyle]}>
|
|
258
|
+
{footerContent}
|
|
259
|
+
</View>
|
|
260
|
+
</View>
|
|
261
|
+
) : null}
|
|
262
|
+
<BottomSheetModal
|
|
263
|
+
ref={bottomSheetModalRef}
|
|
264
|
+
enableDynamicSizing={true}
|
|
265
|
+
snapPoints={image || fullscreen ? ['90%'] : props.snapPoints}
|
|
266
|
+
showHandle={typeof loading !== 'undefined' && loading ? false : props.showHandle}
|
|
267
|
+
accessible={false}
|
|
268
|
+
style={styles.modal}
|
|
269
|
+
footerComponent={stickyFooter && shouldShowFooter ? renderFooter : undefined}
|
|
270
|
+
{...props}
|
|
271
|
+
onChange={handleChange}
|
|
272
|
+
>
|
|
273
|
+
{loading ? <View style={styles.loadingTop} /> : null}
|
|
274
|
+
<BottomSheetScrollView
|
|
275
|
+
contentContainerStyle={[
|
|
276
|
+
styles.scrollView,
|
|
277
|
+
stickyFooter && shouldShowFooter && stickyFooterHeight > 0
|
|
278
|
+
? { paddingBottom: stickyFooterHeight + theme.components.modal.gap }
|
|
279
|
+
: null,
|
|
280
|
+
]}
|
|
281
|
+
ref={scrollViewRef}
|
|
282
|
+
>
|
|
283
|
+
{content}
|
|
284
|
+
</BottomSheetScrollView>
|
|
285
|
+
</BottomSheetModal>
|
|
286
|
+
</>
|
|
244
287
|
);
|
|
245
288
|
};
|
|
246
289
|
|
|
@@ -262,14 +305,6 @@ const styles = StyleSheet.create((theme, rt) => ({
|
|
|
262
305
|
scrollView: {
|
|
263
306
|
flex: 1,
|
|
264
307
|
variants: {
|
|
265
|
-
bothButtons: {
|
|
266
|
-
true: {
|
|
267
|
-
paddingBottom: 166,
|
|
268
|
-
},
|
|
269
|
-
false: {
|
|
270
|
-
paddingBottom: 102,
|
|
271
|
-
},
|
|
272
|
-
},
|
|
273
308
|
noButtons: {
|
|
274
309
|
true: {
|
|
275
310
|
paddingBottom: theme.components.modal.padding,
|
|
@@ -287,31 +322,10 @@ const styles = StyleSheet.create((theme, rt) => ({
|
|
|
287
322
|
},
|
|
288
323
|
},
|
|
289
324
|
compoundVariants: [
|
|
290
|
-
{
|
|
291
|
-
bothButtons: true,
|
|
292
|
-
useSafeAreaInsets: true,
|
|
293
|
-
styles: {
|
|
294
|
-
paddingBottom:
|
|
295
|
-
166 +
|
|
296
|
-
rt.insets.bottom -
|
|
297
|
-
theme.components.modal.padding +
|
|
298
|
-
theme.components.bottomSheet.padding,
|
|
299
|
-
},
|
|
300
|
-
},
|
|
301
|
-
{
|
|
302
|
-
bothButtons: false,
|
|
303
|
-
useSafeAreaInsets: true,
|
|
304
|
-
styles: {
|
|
305
|
-
paddingBottom:
|
|
306
|
-
102 +
|
|
307
|
-
rt.insets.bottom -
|
|
308
|
-
theme.components.modal.padding +
|
|
309
|
-
theme.components.bottomSheet.padding,
|
|
310
|
-
},
|
|
311
|
-
},
|
|
312
325
|
{
|
|
313
326
|
noButtons: true,
|
|
314
327
|
useSafeAreaInsets: true,
|
|
328
|
+
stickyFooter: false,
|
|
315
329
|
styles: {
|
|
316
330
|
paddingBottom:
|
|
317
331
|
rt.insets.bottom +
|
|
@@ -372,6 +386,12 @@ const styles = StyleSheet.create((theme, rt) => ({
|
|
|
372
386
|
footer: {
|
|
373
387
|
gap: theme.components.modal.action.gap,
|
|
374
388
|
},
|
|
389
|
+
footerMeasurementContainer: {
|
|
390
|
+
left: 0,
|
|
391
|
+
opacity: 0,
|
|
392
|
+
position: 'absolute',
|
|
393
|
+
right: 0,
|
|
394
|
+
},
|
|
375
395
|
footerWrap: {
|
|
376
396
|
backgroundColor: theme.color.surface.neutral.strong,
|
|
377
397
|
paddingHorizontal: theme.components.bottomSheet.padding,
|
|
@@ -31,6 +31,8 @@ const Modal = ({
|
|
|
31
31
|
loadingHeading = 'Loading...',
|
|
32
32
|
fullscreen = false,
|
|
33
33
|
image,
|
|
34
|
+
footer,
|
|
35
|
+
footerStyle,
|
|
34
36
|
primaryButtonProps,
|
|
35
37
|
secondaryButtonProps,
|
|
36
38
|
closeButtonProps,
|
|
@@ -91,7 +93,32 @@ const Modal = ({
|
|
|
91
93
|
}
|
|
92
94
|
};
|
|
93
95
|
|
|
94
|
-
const
|
|
96
|
+
const hasPrimaryButton = !!(onPressPrimaryButton && primaryButtonText);
|
|
97
|
+
const hasSecondaryButton = !!(onPressSecondaryButton && secondaryButtonText);
|
|
98
|
+
const hasFooter = !!footer || hasPrimaryButton || hasSecondaryButton;
|
|
99
|
+
|
|
100
|
+
const footerContent = footer ?? (
|
|
101
|
+
<View style={styles.footer}>
|
|
102
|
+
{hasPrimaryButton ? (
|
|
103
|
+
<Button
|
|
104
|
+
onPress={handlePrimaryButtonPress}
|
|
105
|
+
text={primaryButtonText}
|
|
106
|
+
{...primaryButtonProps}
|
|
107
|
+
variant={(primaryButtonProps?.variant as 'solid') ?? 'solid'}
|
|
108
|
+
colorScheme={(primaryButtonProps?.colorScheme as 'highlight') ?? 'highlight'}
|
|
109
|
+
/>
|
|
110
|
+
) : null}
|
|
111
|
+
{hasSecondaryButton ? (
|
|
112
|
+
<Button
|
|
113
|
+
onPress={handleSecondaryButtonPress}
|
|
114
|
+
text={secondaryButtonText}
|
|
115
|
+
{...secondaryButtonProps}
|
|
116
|
+
variant={(secondaryButtonProps?.variant as 'outline') ?? 'outline'}
|
|
117
|
+
colorScheme={(secondaryButtonProps?.colorScheme as 'functional') ?? 'functional'}
|
|
118
|
+
/>
|
|
119
|
+
) : null}
|
|
120
|
+
</View>
|
|
121
|
+
);
|
|
95
122
|
|
|
96
123
|
const content = (
|
|
97
124
|
<>
|
|
@@ -152,28 +179,7 @@ const Modal = ({
|
|
|
152
179
|
</View>
|
|
153
180
|
) : null}
|
|
154
181
|
{children}
|
|
155
|
-
{
|
|
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}
|
|
182
|
+
{hasFooter ? <View style={footerStyle}>{footerContent}</View> : null}
|
|
177
183
|
</View>
|
|
178
184
|
)}
|
|
179
185
|
</>
|