@utilitywarehouse/hearth-react-native 0.29.2 → 0.30.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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-lint.log +13 -13
- package/CHANGELOG.md +67 -0
- package/build/components/DescriptionList/DescriptionListItem.js +1 -2
- package/build/components/ExpandableCard/ExpandableCardTriggerRoot.js +2 -2
- package/build/components/Heading/Heading.js +1 -1
- package/build/components/List/List.js +2 -2
- package/build/components/List/ListAction/ListAction.js +1 -1
- package/build/components/List/ListItem/ListItemRoot.js +2 -2
- package/build/components/Modal/Modal.d.ts +1 -1
- package/build/components/Modal/Modal.js +2 -2
- package/build/components/Modal/Modal.shared.types.d.ts +1 -0
- package/build/components/NavModal/NavModal.d.ts +1 -1
- package/build/components/NavModal/NavModal.js +51 -11
- package/build/components/NavModal/NavModal.props.d.ts +3 -3
- package/docs/changelog.mdx +86 -0
- package/package.json +3 -3
- package/src/components/DescriptionList/DescriptionListItem.tsx +1 -2
- package/src/components/ExpandableCard/ExpandableCardTriggerRoot.tsx +4 -4
- package/src/components/Heading/Heading.docs.mdx +12 -3
- package/src/components/Heading/Heading.tsx +1 -0
- package/src/components/List/List.docs.mdx +2 -2
- package/src/components/List/List.stories.tsx +6 -5
- package/src/components/List/List.tsx +14 -2
- package/src/components/List/ListAction/ListAction.tsx +1 -0
- package/src/components/List/ListItem/ListItemRoot.tsx +3 -3
- package/src/components/Modal/Modal.docs.mdx +22 -21
- package/src/components/Modal/Modal.shared.types.ts +1 -0
- package/src/components/Modal/Modal.tsx +6 -1
- package/src/components/NavModal/NavModal.docs.mdx +24 -23
- package/src/components/NavModal/NavModal.props.ts +3 -3
- package/src/components/NavModal/NavModal.tsx +61 -16
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.30.1 lint /home/runner/work/hearth/hearth/packages/react-native
|
|
3
3
|
> TIMING=1 eslint .
|
|
4
4
|
|
|
5
5
|
|
|
@@ -51,15 +51,15 @@
|
|
|
51
51
|
|
|
52
52
|
✖ 22 problems (0 errors, 22 warnings)
|
|
53
53
|
|
|
54
|
-
Rule
|
|
55
|
-
|
|
56
|
-
@typescript-eslint/no-unused-vars
|
|
57
|
-
react-hooks/exhaustive-deps
|
|
58
|
-
no-global-assign
|
|
59
|
-
react-hooks/rules-of-hooks
|
|
60
|
-
no-
|
|
61
|
-
no-unexpected-multiline
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
no-useless-escape
|
|
65
|
-
|
|
54
|
+
Rule | Time (ms) | Relative
|
|
55
|
+
:---------------------------------|----------:|--------:
|
|
56
|
+
@typescript-eslint/no-unused-vars | 1663.735 | 62.2%
|
|
57
|
+
react-hooks/exhaustive-deps | 114.322 | 4.3%
|
|
58
|
+
no-global-assign | 106.232 | 4.0%
|
|
59
|
+
react-hooks/rules-of-hooks | 88.095 | 3.3%
|
|
60
|
+
no-misleading-character-class | 76.056 | 2.8%
|
|
61
|
+
no-unexpected-multiline | 42.483 | 1.6%
|
|
62
|
+
@typescript-eslint/ban-ts-comment | 34.247 | 1.3%
|
|
63
|
+
no-loss-of-precision | 32.608 | 1.2%
|
|
64
|
+
no-useless-escape | 30.291 | 1.1%
|
|
65
|
+
no-control-regex | 26.605 | 1.0%
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,72 @@
|
|
|
1
1
|
# @utilitywarehouse/hearth-react-native
|
|
2
2
|
|
|
3
|
+
## 0.30.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#1081](https://github.com/utilitywarehouse/hearth/pull/1081) [`5db8538`](https://github.com/utilitywarehouse/hearth/commit/5db8538b69115a23289f0038f681fc8b87a310c4) Thanks [@jordmccord](https://github.com/jordmccord)! - 🐛 [FIX]: Correct `NavModal` safe area handling across sheet and full-screen presentations.
|
|
8
|
+
|
|
9
|
+
`NavModal` now applies safe area insets directly within the component layout, which fixes padding in full-screen presentations and keeps sheet-style presentations aligned with the modal footer behaviour.
|
|
10
|
+
|
|
11
|
+
**Developer changes**:
|
|
12
|
+
|
|
13
|
+
If you need to disable the inset padding, use the `useSafeAreaInsets` prop. The old `safeAreaViewProps` escape hatch is no longer available.
|
|
14
|
+
|
|
15
|
+
## 0.30.0
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- [#1072](https://github.com/utilitywarehouse/hearth/pull/1072) [`55f0095`](https://github.com/utilitywarehouse/hearth/commit/55f009576ba55081de358bccc21691861ddd7c33) Thanks [@jordmccord](https://github.com/jordmccord)! - 🌟 [FEATURE]: Add `loadingDescription` support to `Modal` and `NavModal`
|
|
20
|
+
|
|
21
|
+
`Modal` and `NavModal` now accept a `loadingDescription` prop to render supporting text beneath the spinner while `loading` is true. This makes it easier to give users more context during loading states without building custom loading content.
|
|
22
|
+
|
|
23
|
+
**Components affected**:
|
|
24
|
+
- `Modal`
|
|
25
|
+
- `NavModal`
|
|
26
|
+
|
|
27
|
+
**Developer changes**:
|
|
28
|
+
|
|
29
|
+
No changes are required for existing usage. To show supporting text in a loading state, pass `loadingDescription` alongside `loading` and, if needed, `loadingHeading`.
|
|
30
|
+
|
|
31
|
+
### Patch Changes
|
|
32
|
+
|
|
33
|
+
- [#1070](https://github.com/utilitywarehouse/hearth/pull/1070) [`93c042c`](https://github.com/utilitywarehouse/hearth/commit/93c042c7772ab298e2ea4888a9777e8176453098) Thanks [@jordmccord](https://github.com/jordmccord)! - 🐛 [FIX]: Standardise numeric value typography across list-based components.
|
|
34
|
+
|
|
35
|
+
Numeric values in `DescriptionListItem`, `ExpandableCard`, and `ListItem` now render with semibold `BodyText` instead of `DetailText`, aligning them with the updated content hierarchy used elsewhere in the library.
|
|
36
|
+
|
|
37
|
+
**Components affected**:
|
|
38
|
+
- `DescriptionListItem`
|
|
39
|
+
- `ExpandableCard`
|
|
40
|
+
- `ListItem`
|
|
41
|
+
|
|
42
|
+
**Developer changes**:
|
|
43
|
+
|
|
44
|
+
No changes are required.
|
|
45
|
+
|
|
46
|
+
- [#1073](https://github.com/utilitywarehouse/hearth/pull/1073) [`9759622`](https://github.com/utilitywarehouse/hearth/commit/975962229137dd196e5f72a04037a8f181907818) Thanks [@jordmccord](https://github.com/jordmccord)! - 🐛 [FIX]: Announce `Heading` as a header for assistive technologies.
|
|
47
|
+
|
|
48
|
+
`Heading` now sets `accessibilityRole="header"` automatically so VoiceOver and TalkBack can identify headings as part of the screen structure.
|
|
49
|
+
|
|
50
|
+
**Components affected**:
|
|
51
|
+
- `Heading`
|
|
52
|
+
|
|
53
|
+
**Developer changes**:
|
|
54
|
+
|
|
55
|
+
No changes are required.
|
|
56
|
+
|
|
57
|
+
- [#1074](https://github.com/utilitywarehouse/hearth/pull/1074) [`95fe19e`](https://github.com/utilitywarehouse/hearth/commit/95fe19e6328bf652ff3ac1b2c723e1389fc59936) Thanks [@jordmccord](https://github.com/jordmccord)! - 🐛 [FIX]: Improve accessibility roles for `List`, `ListItem`, and `ListAction`
|
|
58
|
+
|
|
59
|
+
`List` now defaults to `accessibilityRole="list"`, `ListAction` now defaults to `accessibilityRole="button"`, and `ListItem` respects an explicitly provided `accessibilityRole` instead of always forcing button semantics when `onPress` is set.
|
|
60
|
+
|
|
61
|
+
**Components affected**:
|
|
62
|
+
- `List`
|
|
63
|
+
- `ListItem`
|
|
64
|
+
- `ListAction`
|
|
65
|
+
|
|
66
|
+
**Developer changes**:
|
|
67
|
+
|
|
68
|
+
No changes are required unless you want a tappable `ListItem` to be announced as something other than a button. In that case, pass `accessibilityRole` explicitly.
|
|
69
|
+
|
|
3
70
|
## 0.29.2
|
|
4
71
|
|
|
5
72
|
### Patch Changes
|
|
@@ -4,7 +4,6 @@ import { View } from 'react-native';
|
|
|
4
4
|
import { StyleSheet } from 'react-native-unistyles';
|
|
5
5
|
import { useTheme } from '../../hooks';
|
|
6
6
|
import { BodyText } from '../BodyText';
|
|
7
|
-
import { DetailText } from '../DetailText';
|
|
8
7
|
import Helper from '../Helper/Helper';
|
|
9
8
|
import { useDescriptionListContext } from './DescriptionList.context';
|
|
10
9
|
const DescriptionListItem = ({ heading, description, headingWidth, trailingContent, invalidText, numericValue, style, ...props }) => {
|
|
@@ -15,7 +14,7 @@ const DescriptionListItem = ({ heading, description, headingWidth, trailingConte
|
|
|
15
14
|
const descIsText = typeof description === 'string' || typeof description === 'number';
|
|
16
15
|
const combinedLabel = headingIsText && descIsText ? `${heading}: ${description}` : undefined;
|
|
17
16
|
const hideDescendants = !!combinedLabel;
|
|
18
|
-
return (_jsxs(View, { accessibilityRole: "text", accessible: !!combinedLabel, accessibilityLabel: combinedLabel, ...props, style: [styles.item, style], children: [_jsxs(View, { style: styles.textWrap, importantForAccessibility: hideDescendants ? 'no-hide-descendants' : undefined, accessibilityElementsHidden: hideDescendants || undefined, children: [_jsx(View, { style: [direction === 'row' && { width: headingWidth || itemHeadingWidth }], children: headingIsText ? _jsx(BodyText, { style: styles.headingText, children: heading }) : heading }), _jsxs(View, { style: styles.descriptionWrapper, children: [descIsText ? _jsx(BodyText, { children: description }) : description, !!invalidText && (_jsx(Helper, { validationStatus: "invalid", showIcon: true, icon: ErrorCircleSmallIcon, text: invalidText || '' }))] }), numericValue ? (_jsx(View, { style: styles.descriptionWrapper, children: _jsx(
|
|
17
|
+
return (_jsxs(View, { accessibilityRole: "text", accessible: !!combinedLabel, accessibilityLabel: combinedLabel, ...props, style: [styles.item, style], children: [_jsxs(View, { style: styles.textWrap, importantForAccessibility: hideDescendants ? 'no-hide-descendants' : undefined, accessibilityElementsHidden: hideDescendants || undefined, children: [_jsx(View, { style: [direction === 'row' && { width: headingWidth || itemHeadingWidth }], children: headingIsText ? _jsx(BodyText, { style: styles.headingText, children: heading }) : heading }), _jsxs(View, { style: styles.descriptionWrapper, children: [descIsText ? _jsx(BodyText, { children: description }) : description, !!invalidText && (_jsx(Helper, { validationStatus: "invalid", showIcon: true, icon: ErrorCircleSmallIcon, text: invalidText || '' }))] }), numericValue ? (_jsx(View, { style: styles.descriptionWrapper, children: _jsx(BodyText, { weight: "semibold", children: numericValue }) })) : null] }), trailingContent ? trailingContent : null] }));
|
|
19
18
|
};
|
|
20
19
|
DescriptionListItem.displayName = 'DescriptionListItem';
|
|
21
20
|
const styles = StyleSheet.create(theme => ({
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { ChevronDownSmallIcon, ChevronUpSmallIcon, } from '@utilitywarehouse/hearth-react-native-icons';
|
|
3
3
|
import { Pressable } from 'react-native';
|
|
4
4
|
import { StyleSheet } from 'react-native-unistyles';
|
|
5
|
-
import {
|
|
5
|
+
import { BodyText } from '../BodyText';
|
|
6
6
|
import ExpandableCardContent from './ExpandableCardContent';
|
|
7
7
|
import ExpandableCardHelperText from './ExpandableCardHelperText';
|
|
8
8
|
import ExpandableCardIcon from './ExpandableCardIcon';
|
|
@@ -26,7 +26,7 @@ const ExpandableCardTriggerRoot = ({ heading, helperText, leadingIcon, leadingCo
|
|
|
26
26
|
}
|
|
27
27
|
return null;
|
|
28
28
|
};
|
|
29
|
-
const renderDefaultContent = () => (_jsxs(_Fragment, { children: [renderLeadingContent(), _jsxs(ExpandableCardContent, { children: [badgePosition === 'top' ? badge : null, _jsx(ExpandableCardText, { children: heading }), helperText && _jsx(ExpandableCardHelperText, { children: helperText }), badgePosition === 'bottom' ? badge : null] }), numericValue && (_jsx(
|
|
29
|
+
const renderDefaultContent = () => (_jsxs(_Fragment, { children: [renderLeadingContent(), _jsxs(ExpandableCardContent, { children: [badgePosition === 'top' ? badge : null, _jsx(ExpandableCardText, { children: heading }), helperText && _jsx(ExpandableCardHelperText, { children: helperText }), badgePosition === 'bottom' ? badge : null] }), !!numericValue && (_jsx(BodyText, { weight: "semibold", style: styles.numericValue, children: numericValue })), _jsx(ExpandableCardTrailingContent, { style: styles.chevron, children: _jsx(ExpandableCardTrailingIcon, { as: isExpanded ? ChevronUpSmallIcon : ChevronDownSmallIcon }) })] }));
|
|
30
30
|
return (_jsx(Pressable, { ...props, testID: testID, style: [styles.container, props.style], disabled: disabled, accessibilityRole: "button", accessibilityState: { expanded: isExpanded, disabled }, accessibilityLabel: `${heading}${helperText ? `, ${helperText}` : ''}`, children: children || renderDefaultContent() }));
|
|
31
31
|
};
|
|
32
32
|
ExpandableCardTriggerRoot.displayName = 'ExpandableCardTriggerRoot';
|
|
@@ -12,7 +12,7 @@ const Heading = ({ children, color, size = 'lg', truncated, underline, strikeThr
|
|
|
12
12
|
strikeThrough,
|
|
13
13
|
inverted,
|
|
14
14
|
});
|
|
15
|
-
return (_jsx(Text, { ...remainingProps, ...(truncated
|
|
15
|
+
return (_jsx(Text, { accessibilityRole: "header", ...remainingProps, ...(truncated
|
|
16
16
|
? {
|
|
17
17
|
numberOfLines: 1,
|
|
18
18
|
ellipsizeMode: 'tail',
|
|
@@ -6,7 +6,7 @@ import { Card } from '../Card';
|
|
|
6
6
|
import { SectionHeader } from '../SectionHeader';
|
|
7
7
|
import { ListContext } from './List.context';
|
|
8
8
|
const List = ({ children, heading, helperText, headerTrailingContent, invalidText, ...props }) => {
|
|
9
|
-
const { loading, disabled, container = 'none', testID, style, ...rest } = props;
|
|
9
|
+
const { loading, disabled, container = 'none', testID, style, accessibilityRole, ...rest } = props;
|
|
10
10
|
const orderRef = useRef([]);
|
|
11
11
|
const [firstItemId, setFirstItemId] = useState(undefined);
|
|
12
12
|
const containerToCard = {
|
|
@@ -35,7 +35,7 @@ const List = ({ children, heading, helperText, headerTrailingContent, invalidTex
|
|
|
35
35
|
registerItem,
|
|
36
36
|
};
|
|
37
37
|
styles.useVariants({ disabled });
|
|
38
|
-
return (_jsx(ListContext.Provider, { value: value, children: _jsxs(View, { ...rest, style: [styles.container, style], children: [heading ? (_jsx(SectionHeader, { heading: heading, helperText: helperText, trailingContent: headerTrailingContent, invalidText: invalidText })) : null, container === 'none' ? (_jsx(View, { testID: testID, children: children })) : (React.Children.count(children) > 0 && (_jsx(Card, { ...containerToCard, noPadding: true, style: styles.card, testID: testID, children: _jsx(_Fragment, { children: children }) })))] }) }));
|
|
38
|
+
return (_jsx(ListContext.Provider, { value: value, children: _jsxs(View, { ...rest, accessibilityRole: accessibilityRole ?? 'list', style: [styles.container, style], children: [heading ? (_jsx(SectionHeader, { heading: heading, helperText: helperText, trailingContent: headerTrailingContent, invalidText: invalidText })) : null, container === 'none' ? (_jsx(View, { testID: testID, children: children })) : (React.Children.count(children) > 0 && (_jsx(Card, { ...containerToCard, noPadding: true, style: styles.card, testID: testID, children: _jsx(_Fragment, { children: children }) })))] }) }));
|
|
39
39
|
};
|
|
40
40
|
List.displayName = 'List';
|
|
41
41
|
const styles = StyleSheet.create(theme => ({
|
|
@@ -43,7 +43,7 @@ const ListActionRoot = ({ heading, disabled, variant = 'subtle', loading, ...pro
|
|
|
43
43
|
isFirstChild,
|
|
44
44
|
container: listContext?.container,
|
|
45
45
|
});
|
|
46
|
-
return (_jsx(Pressable, { ...props, testID: testID, style: [styles.container, props.style], disabled: isDisabled || !onPress, children: loading ? (_jsxs(View, { style: styles.loadingWrap, children: [_jsx(Skeleton, { style: { flex: 1, maxWidth: 166 }, width: "auto", height: 24, borderRadius: "sm" }), _jsx(Skeleton, { width: 24, height: 24, borderRadius: "sm" })] })) : (_jsxs(_Fragment, { children: [_jsx(ListActionContent, { children: _jsx(ListActionText, { children: heading }) }), _jsx(ListActionTrailingContent, { style: styles.centeredTrailingIcon, children: _jsx(ListActionTrailingIcon, { as: ChevronRightSmallIcon }) })] })) }));
|
|
46
|
+
return (_jsx(Pressable, { ...props, accessibilityRole: props.accessibilityRole ?? 'button', testID: testID, style: [styles.container, props.style], disabled: isDisabled || !onPress, children: loading ? (_jsxs(View, { style: styles.loadingWrap, children: [_jsx(Skeleton, { style: { flex: 1, maxWidth: 166 }, width: "auto", height: 24, borderRadius: "sm" }), _jsx(Skeleton, { width: 24, height: 24, borderRadius: "sm" })] })) : (_jsxs(_Fragment, { children: [_jsx(ListActionContent, { children: _jsx(ListActionText, { children: heading }) }), _jsx(ListActionTrailingContent, { style: styles.centeredTrailingIcon, children: _jsx(ListActionTrailingIcon, { as: ChevronRightSmallIcon }) })] })) }));
|
|
47
47
|
};
|
|
48
48
|
const ListAction = createPressable({
|
|
49
49
|
Root: ListActionRoot,
|
|
@@ -3,7 +3,7 @@ import { ChevronRightSmallIcon } from '@utilitywarehouse/hearth-react-native-ico
|
|
|
3
3
|
import { useId, useLayoutEffect, useMemo } from 'react';
|
|
4
4
|
import { Pressable } from 'react-native';
|
|
5
5
|
import { StyleSheet } from 'react-native-unistyles';
|
|
6
|
-
import {
|
|
6
|
+
import { BodyText } from '../../BodyText';
|
|
7
7
|
import { Skeleton } from '../../Skeleton';
|
|
8
8
|
import { useListContext } from '../List.context';
|
|
9
9
|
import { ListItemContext } from './ListItem.context';
|
|
@@ -61,7 +61,7 @@ const ListItemRoot = ({ heading, helperText, leadingContent, trailingContent, di
|
|
|
61
61
|
if (loading || listContext?.loading) {
|
|
62
62
|
return (_jsxs(Pressable, { ...props, testID: loadingTestID, style: [styles.container, props.style], disabled: isDisabled, children: [leadingContent ? _jsx(Skeleton, { width: 24, height: 24 }) : null, _jsxs(ListItemContent, { children: [_jsx(Skeleton, { width: "80%", height: 20 }), _jsx(Skeleton, { width: "100%", height: 16 })] }), onPress || trailingContent ? _jsx(Skeleton, { width: 24, height: 24 }) : null] }));
|
|
63
63
|
}
|
|
64
|
-
return (_jsx(ListItemContext.Provider, { value: value, children: _jsx(Pressable, { ...props, testID: testID, style: [styles.container, props.style], disabled: isDisabled, accessibilityRole: onPress ? 'button' : undefined, children: children ? (children) : (_jsxs(_Fragment, { children: [leadingContent ? (_jsx(ListItemLeadingContent, { children: leadingContent })) : null, _jsxs(ListItemContent, { children: [badgePosition === 'top' && badge ? badge : null, _jsx(ListItemHeading, { truncated: truncateHeading, children: heading }), helperText ? (_jsx(ListItemHelperText, { truncated: truncateHelperText, children: helperText })) : null, badgePosition === 'bottom' && badge ? badge : null] }), !!numericValue && _jsx(
|
|
64
|
+
return (_jsx(ListItemContext.Provider, { value: value, children: _jsx(Pressable, { ...props, testID: testID, style: [styles.container, props.style], disabled: isDisabled, accessibilityRole: props.accessibilityRole ?? (onPress ? 'button' : undefined), children: children ? (children) : (_jsxs(_Fragment, { children: [leadingContent ? (_jsx(ListItemLeadingContent, { children: leadingContent })) : null, _jsxs(ListItemContent, { children: [badgePosition === 'top' && badge ? badge : null, _jsx(ListItemHeading, { truncated: truncateHeading, children: heading }), helperText ? (_jsx(ListItemHelperText, { truncated: truncateHelperText, children: helperText })) : null, badgePosition === 'bottom' && badge ? badge : null] }), !!numericValue && _jsx(BodyText, { weight: "semibold", children: numericValue }), trailingContent ? (_jsx(ListItemTrailingContent, { children: trailingContent })) : onPress ? (_jsx(ListItemTrailingContent, { style: styles.centeredTrailingIcon, children: _jsx(ListItemTrailingIcon, { as: ChevronRightSmallIcon }) })) : null] })) }) }));
|
|
65
65
|
};
|
|
66
66
|
ListItemRoot.displayName = 'ListItemRoot';
|
|
67
67
|
const styles = StyleSheet.create(theme => ({
|
|
@@ -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, stickyFooter, ...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, loadingDescription, fullscreen, image, primaryButtonProps, secondaryButtonProps, closeButtonProps, stickyFooter, ...props }: ModalProps) => import("react/jsx-runtime").JSX.Element;
|
|
7
7
|
export default Modal;
|
|
@@ -11,7 +11,7 @@ import { Button } from '../Button';
|
|
|
11
11
|
import { Heading } from '../Heading';
|
|
12
12
|
import { Spinner } from '../Spinner';
|
|
13
13
|
import { UnstyledIconButton } from '../UnstyledIconButton';
|
|
14
|
-
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, stickyFooter = true, ...props }) => {
|
|
14
|
+
const Modal = ({ ref, children, heading, description, showCloseButton = true, primaryButtonText, secondaryButtonText, onPressPrimaryButton, onPressCloseButton, onPressSecondaryButton, closeOnPrimaryButtonPress = true, closeOnSecondaryButtonPress = true, loading, loadingHeading = 'Loading...', loadingDescription, fullscreen = false, image, primaryButtonProps, secondaryButtonProps, closeButtonProps, stickyFooter = true, ...props }) => {
|
|
15
15
|
const bottomSheetModalRef = useRef(null);
|
|
16
16
|
const viewRef = useRef(null);
|
|
17
17
|
const scrollViewRef = useRef(null);
|
|
@@ -78,7 +78,7 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
|
|
|
78
78
|
secondaryButtonProps,
|
|
79
79
|
secondaryButtonText,
|
|
80
80
|
]);
|
|
81
|
-
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", ...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, children: heading })) : null, description ? (_jsx(BodyText, { textAlign: "center", accessible: true, children: description })) : null] })] })) : null, children, !stickyFooter && !noButtons ? footer : null] })) }));
|
|
81
|
+
const content = (_jsx(_Fragment, { children: loading ? (_jsxs(View, { style: styles.loadingContainer, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? (loadingHeading ?? 'Loading') : undefined, accessibilityHint: Platform.OS === 'android' && loadingDescription ? loadingDescription : undefined, screenReaderFocusable: true, ref: viewRef, children: [_jsx(Spinner, { size: "lg" }), _jsx(Heading, { size: "lg", textAlign: "center", children: loadingHeading }), loadingDescription ? _jsx(BodyText, { textAlign: "center", children: loadingDescription }) : null] })) : (_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", ...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, children: heading })) : null, description ? (_jsx(BodyText, { textAlign: "center", accessible: true, children: description })) : null] })] })) : null, children, !stickyFooter && !noButtons ? footer : null] })) }));
|
|
82
82
|
const renderFooter = useCallback((props) => (_jsx(BottomSheetFooter, { ...props, children: _jsx(View, { style: styles.footerWrap, children: footer }) })), [footer]);
|
|
83
83
|
return (_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 })] }));
|
|
84
84
|
};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import NavModalProps from './NavModal.props';
|
|
2
|
-
declare const NavModal: ({ ref, children, heading, description, showCloseButton, primaryButtonText, secondaryButtonText, onPressPrimaryButton, onPressCloseButton, onPressSecondaryButton, loading, loadingHeading, image, primaryButtonProps, secondaryButtonProps, closeButtonProps, stickyFooter, background, scrollable, presentation, scrollViewProps,
|
|
2
|
+
declare const NavModal: ({ ref, children, heading, description, showCloseButton, primaryButtonText, secondaryButtonText, onPressPrimaryButton, onPressCloseButton, onPressSecondaryButton, loading, loadingHeading, loadingDescription, image, primaryButtonProps, secondaryButtonProps, closeButtonProps, stickyFooter, background, scrollable, presentation, scrollViewProps, useSafeAreaInsets, ...props }: NavModalProps) => import("react/jsx-runtime").JSX.Element;
|
|
3
3
|
export default NavModal;
|
|
@@ -3,7 +3,6 @@ import { CloseMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
|
|
|
3
3
|
import { useCallback, useEffect, useImperativeHandle, useMemo } from 'react';
|
|
4
4
|
import { Platform, ScrollView, View } from 'react-native';
|
|
5
5
|
import Animated, { Easing, useAnimatedStyle, useSharedValue, withDelay, withTiming, } from 'react-native-reanimated';
|
|
6
|
-
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
7
6
|
import { StyleSheet } from 'react-native-unistyles';
|
|
8
7
|
import { useTheme } from '../../hooks';
|
|
9
8
|
import { hexWithOpacity } from '../../utils';
|
|
@@ -12,7 +11,7 @@ import { Button } from '../Button';
|
|
|
12
11
|
import { Heading } from '../Heading';
|
|
13
12
|
import { Spinner } from '../Spinner';
|
|
14
13
|
import { UnstyledIconButton } from '../UnstyledIconButton';
|
|
15
|
-
const NavModal = ({ ref, children, heading, description, showCloseButton = true, primaryButtonText, secondaryButtonText, onPressPrimaryButton, onPressCloseButton, onPressSecondaryButton, loading, loadingHeading = 'Loading...', image, primaryButtonProps, secondaryButtonProps, closeButtonProps, stickyFooter = true, background = 'default', scrollable = true, presentation = 'modal', scrollViewProps,
|
|
14
|
+
const NavModal = ({ ref, children, heading, description, showCloseButton = true, primaryButtonText, secondaryButtonText, onPressPrimaryButton, onPressCloseButton, onPressSecondaryButton, loading, loadingHeading = 'Loading...', loadingDescription, image, primaryButtonProps, secondaryButtonProps, closeButtonProps, stickyFooter = true, background = 'default', scrollable = true, presentation = 'modal', scrollViewProps, useSafeAreaInsets = true, ...props }) => {
|
|
16
15
|
const theme = useTheme();
|
|
17
16
|
const backgroundOpacity = useSharedValue(0);
|
|
18
17
|
const pretendContentTranslateY = useSharedValue(20);
|
|
@@ -66,6 +65,9 @@ const NavModal = ({ ref, children, heading, description, showCloseButton = true,
|
|
|
66
65
|
loading,
|
|
67
66
|
background: isBrandBackground ? 'brand' : 'primary',
|
|
68
67
|
presentation: isFullScreenPresentation ? 'fullScreen' : 'modal',
|
|
68
|
+
useSafeAreaInsets,
|
|
69
|
+
usesSheetPresentation,
|
|
70
|
+
stickyFooter,
|
|
69
71
|
});
|
|
70
72
|
const footer = useMemo(() => (_jsxs(View, { style: styles.footer, children: [onPressPrimaryButton && primaryButtonText ? (_jsx(Button, { onPress: handlePrimaryButtonPress, text: primaryButtonText, inverted: isBrandBackground, ...primaryButtonProps, variant: primaryButtonProps?.variant ?? 'solid', colorScheme: primaryButtonProps?.colorScheme ?? 'highlight' })) : null, onPressSecondaryButton && secondaryButtonText ? (_jsx(Button, { onPress: handleSecondaryButtonPress, text: secondaryButtonText, inverted: isBrandBackground, ...secondaryButtonProps, variant: secondaryButtonProps?.variant ?? 'outline', colorScheme: secondaryButtonProps?.colorScheme ?? 'functional' })) : null] })), [
|
|
71
73
|
handlePrimaryButtonPress,
|
|
@@ -78,25 +80,63 @@ const NavModal = ({ ref, children, heading, description, showCloseButton = true,
|
|
|
78
80
|
secondaryButtonProps,
|
|
79
81
|
secondaryButtonText,
|
|
80
82
|
]);
|
|
81
|
-
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, children: [_jsx(Spinner, { size: "lg", color: isBrandBackground ? theme.color.icon.inverted : undefined }), _jsx(Heading, { size: "lg", textAlign: "center", inverted: isBrandBackground, children: loadingHeading })] })) : (_jsxs(View, { style: styles.container, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Modal content' : undefined, screenReaderFocusable: true, children: [_jsxs(View, { style: styles.header, children: [_jsxs(View, { style: styles.headerTextContent, children: [heading && !image ? (_jsx(Heading, { size: "lg", accessible: true, inverted: isBrandBackground, children: heading })) : null, description && !image ? (_jsx(BodyText, { accessible: true, inverted: isBrandBackground, children: description })) : null] }), showCloseButton ? (_jsx(UnstyledIconButton, { icon: CloseMediumIcon, onPress: handleCloseButtonPress, accessibilityLabel: "Close modal", inverted: isBrandBackground, ...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, children: heading })) : null, description ? (_jsx(BodyText, { textAlign: "center", accessible: true, inverted: isBrandBackground, children: description })) : null] })] })) : null, scrollable ? (_jsxs(ScrollView, { style: {
|
|
83
|
+
const content = (_jsx(_Fragment, { children: loading ? (_jsxs(View, { style: styles.loadingContainer, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? (loadingHeading ?? 'Loading') : undefined, accessibilityHint: Platform.OS === 'android' && loadingDescription ? loadingDescription : undefined, screenReaderFocusable: true, children: [_jsx(Spinner, { size: "lg", color: isBrandBackground ? theme.color.icon.inverted : undefined }), _jsx(Heading, { size: "lg", textAlign: "center", inverted: isBrandBackground, children: loadingHeading }), loadingDescription ? (_jsx(BodyText, { size: "md", textAlign: "center", inverted: isBrandBackground, children: loadingDescription })) : null] })) : (_jsxs(View, { style: styles.container, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Modal content' : undefined, screenReaderFocusable: true, children: [_jsxs(View, { style: styles.header, children: [_jsxs(View, { style: styles.headerTextContent, children: [heading && !image ? (_jsx(Heading, { size: "lg", accessible: true, inverted: isBrandBackground, children: heading })) : null, description && !image ? (_jsx(BodyText, { accessible: true, inverted: isBrandBackground, children: description })) : null] }), showCloseButton ? (_jsx(UnstyledIconButton, { icon: CloseMediumIcon, onPress: handleCloseButtonPress, accessibilityLabel: "Close modal", inverted: isBrandBackground, ...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, children: heading })) : null, description ? (_jsx(BodyText, { textAlign: "center", accessible: true, inverted: isBrandBackground, children: description })) : null] })] })) : null, scrollable ? (_jsxs(ScrollView, { style: {
|
|
82
84
|
flex: stickyFooter ? 1 : 0,
|
|
83
85
|
marginHorizontal: -4,
|
|
84
86
|
}, contentContainerStyle: { paddingHorizontal: 4 }, ...scrollViewProps, children: [children, !stickyFooter && !noButtons ? (_jsx(View, { style: styles.inNavModalFooterContainer, children: footer })) : null] })) : (_jsxs(View, { style: {
|
|
85
87
|
flex: stickyFooter ? 1 : 0,
|
|
86
88
|
}, children: [children, !stickyFooter && !noButtons ? (_jsx(View, { style: styles.inNavModalFooterContainer, children: footer })) : null] })), stickyFooter && !noButtons ? footer : null] })) }));
|
|
87
|
-
|
|
88
|
-
return (_jsxs(SafeAreaView, { style: [
|
|
89
|
-
{
|
|
90
|
-
flex: 1,
|
|
91
|
-
backgroundColor: theme.color.background[isBrandBackground ? 'brand' : 'secondary'],
|
|
92
|
-
},
|
|
93
|
-
safeAreaViewStyle,
|
|
94
|
-
], ...restSafeAreaViewProps, children: [Platform.OS === 'android' && usesSheetPresentation ? (_jsx(Animated.View, { style: [styles.androidContainer, animatedBackgroundStyle], children: _jsx(Animated.View, { style: [styles.pretendContent, animatedPretendContentStyle] }) })) : null, _jsx(Animated.View, { style: [
|
|
89
|
+
return (_jsxs(View, { style: styles.root, children: [Platform.OS === 'android' && usesSheetPresentation ? (_jsx(Animated.View, { style: [styles.androidContainer, animatedBackgroundStyle], children: _jsx(Animated.View, { style: [styles.pretendContent, animatedPretendContentStyle] }) })) : null, _jsx(Animated.View, { style: [
|
|
95
90
|
styles.inNavModalContainer,
|
|
96
91
|
Platform.OS === 'android' && usesSheetPresentation && animatedBackgroundStyle,
|
|
97
92
|
], ...props, children: _jsx(View, { style: styles.inNavModalContent, children: content }) })] }));
|
|
98
93
|
};
|
|
99
94
|
const styles = StyleSheet.create((theme, rt) => ({
|
|
95
|
+
root: {
|
|
96
|
+
flex: 1,
|
|
97
|
+
variants: {
|
|
98
|
+
background: {
|
|
99
|
+
brand: {
|
|
100
|
+
backgroundColor: theme.color.background.brand,
|
|
101
|
+
},
|
|
102
|
+
primary: {
|
|
103
|
+
backgroundColor: theme.color.background.secondary,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
usesSheetPresentation: {
|
|
107
|
+
true: {},
|
|
108
|
+
false: {},
|
|
109
|
+
},
|
|
110
|
+
useSafeAreaInsets: {
|
|
111
|
+
true: {},
|
|
112
|
+
false: {},
|
|
113
|
+
},
|
|
114
|
+
stickyFooter: {
|
|
115
|
+
true: {},
|
|
116
|
+
false: {},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
compoundVariants: [
|
|
120
|
+
{
|
|
121
|
+
usesSheetPresentation: false,
|
|
122
|
+
useSafeAreaInsets: true,
|
|
123
|
+
styles: {
|
|
124
|
+
paddingTop: rt.insets.top,
|
|
125
|
+
paddingBottom: Platform.OS === 'ios' ? rt.insets.bottom : 0,
|
|
126
|
+
paddingLeft: rt.insets.left,
|
|
127
|
+
paddingRight: rt.insets.right,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
usesSheetPresentation: true,
|
|
132
|
+
useSafeAreaInsets: true,
|
|
133
|
+
stickyFooter: true,
|
|
134
|
+
styles: {
|
|
135
|
+
paddingBottom: Platform.OS === 'ios' ? rt.insets.bottom : 0,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
},
|
|
100
140
|
container: {
|
|
101
141
|
flex: 1,
|
|
102
142
|
gap: theme.components.modal.gap,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Ref } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { ScrollViewProps } from 'react-native';
|
|
3
3
|
import { ModalCommonProps } from '../Modal/Modal.shared.types';
|
|
4
4
|
export interface NavModalRef {
|
|
5
5
|
triggerCloseAnimation?: () => void;
|
|
@@ -9,7 +9,7 @@ interface NavModalProps extends ModalCommonProps {
|
|
|
9
9
|
background?: 'default' | 'brand';
|
|
10
10
|
scrollable?: boolean;
|
|
11
11
|
presentation?: 'fullScreenModal' | 'modal' | 'transparentModal' | 'containedModal' | 'containedTransparentModal';
|
|
12
|
-
|
|
13
|
-
scrollViewProps?: Omit<
|
|
12
|
+
useSafeAreaInsets?: boolean;
|
|
13
|
+
scrollViewProps?: Omit<ScrollViewProps, 'children'>;
|
|
14
14
|
}
|
|
15
15
|
export default NavModalProps;
|
package/docs/changelog.mdx
CHANGED
|
@@ -9,6 +9,92 @@ import { BackToTopButton, NextPrevPage } from './components';
|
|
|
9
9
|
The changelog for the Hearth React Native library. Here you can find all the changes, improvements, and bug fixes for each version.
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
## 0.29.2
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- [#1067](https://github.com/utilitywarehouse/hearth/pull/1067) [`893cbfd`](https://github.com/utilitywarehouse/hearth/commit/893cbfd1bf090b8b75df6f58f2babaf8ba1e0033) Thanks [@jordmccord](https://github.com/jordmccord)! - 🌟 [FEATURE]: Add a `useSafeAreaInsets` prop to `BottomSheetModalProvider` to control Hearth's bottom-sheet safe-area spacing.
|
|
17
|
+
|
|
18
|
+
Bottom-sheet wrappers such as `BottomSheetView`, `BottomSheetScrollView`, `BottomSheetFlatList`, and components that render `SafeAreaView` inside a bottom sheet now respect `BottomSheetModalProvider` configuration.
|
|
19
|
+
|
|
20
|
+
**Components affected**:
|
|
21
|
+
- `BottomSheetModalProvider`
|
|
22
|
+
- `BottomSheetView`
|
|
23
|
+
- `BottomSheetScrollView`
|
|
24
|
+
- `BottomSheetFlatList`
|
|
25
|
+
- `Modal`
|
|
26
|
+
- `Select`
|
|
27
|
+
- `Combobox`
|
|
28
|
+
|
|
29
|
+
**Developer changes**:
|
|
30
|
+
|
|
31
|
+
No changes are required if you want the current behaviour. If your app already applies its own safe-area padding around bottom-sheet content, opt out like this:
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
<BottomSheetModalProvider useSafeAreaInsets={false}>
|
|
35
|
+
{/* Your app content */}
|
|
36
|
+
</BottomSheetModalProvider>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 0.29.1
|
|
40
|
+
|
|
41
|
+
### Patch Changes
|
|
42
|
+
|
|
43
|
+
- [#1062](https://github.com/utilitywarehouse/hearth/pull/1062) [`0da3ffe`](https://github.com/utilitywarehouse/hearth/commit/0da3ffe12691a4287694ae9fcb2290d459e3c041) Thanks [@jordmccord](https://github.com/jordmccord)! - 🐛 [FIX]: Respect the selected `NavModal` background style
|
|
44
|
+
|
|
45
|
+
Fixed an issue where `NavModal` always rendered its inner content with the default surface background, which prevented the `background="brand"` treatment from being applied correctly.
|
|
46
|
+
|
|
47
|
+
**Components affected**:
|
|
48
|
+
- `NavModal`
|
|
49
|
+
|
|
50
|
+
**Developer changes**:
|
|
51
|
+
|
|
52
|
+
No changes are required.
|
|
53
|
+
|
|
54
|
+
## 0.29.0
|
|
55
|
+
|
|
56
|
+
### Minor Changes
|
|
57
|
+
|
|
58
|
+
- [#1060](https://github.com/utilitywarehouse/hearth/pull/1060) [`05d38f9`](https://github.com/utilitywarehouse/hearth/commit/05d38f9414fec6d6b4a0733829b4d290d96fccae) Thanks [@jordmccord](https://github.com/jordmccord)! - 💔 [BREAKING CHANGE]: Extract navigation-presented modal behaviour into `NavModal`
|
|
59
|
+
|
|
60
|
+
`Modal` now only supports the bottom-sheet modal behaviour. The React Navigation screen-based API that was previously exposed through `inNavModal`, `background`, and `scrollable` has moved to the new `NavModal` component.
|
|
61
|
+
|
|
62
|
+
`NavModal` also adds a `presentation` prop so the component can match the React Navigation screen presentation style for sheet-style and full-screen modal routes.
|
|
63
|
+
|
|
64
|
+
The package-owned `SafeAreaView` component has also been removed in favour of the `react-native-safe-area-context` `SafeAreaView`. Hearth now re-exports that implementation from the package root.
|
|
65
|
+
|
|
66
|
+
**Components affected**:
|
|
67
|
+
- `Modal`
|
|
68
|
+
- `NavModal`
|
|
69
|
+
- `SafeAreaView`
|
|
70
|
+
|
|
71
|
+
**Developer changes**:
|
|
72
|
+
|
|
73
|
+
Update navigation modal screens to use `NavModal` instead of `Modal`:
|
|
74
|
+
|
|
75
|
+
```diff
|
|
76
|
+
- import { Modal } from '@utilitywarehouse/hearth-react-native';
|
|
77
|
+
+ import { NavModal, type NavModalRef } from '@utilitywarehouse/hearth-react-native';
|
|
78
|
+
|
|
79
|
+
- const modalRef = useRef<Modal>(null);
|
|
80
|
+
+ const modalRef = useRef<NavModalRef>(null);
|
|
81
|
+
|
|
82
|
+
- <Modal inNavModal background="brand" scrollable={false}>
|
|
83
|
+
+ <NavModal background="brand" scrollable={false} presentation="modal">
|
|
84
|
+
{/* content */}
|
|
85
|
+
- </Modal>
|
|
86
|
+
+ </NavModal>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
If you are using the bottom-sheet modal API, no changes are required.
|
|
90
|
+
|
|
91
|
+
If you were importing the old component from the component path, update it to use the package root re-export or import directly from `react-native-safe-area-context`:
|
|
92
|
+
|
|
93
|
+
```diff
|
|
94
|
+
- import { SafeAreaView } from '@utilitywarehouse/hearth-react-native';
|
|
95
|
+
+ import { SafeAreaView } from 'react-native-safe-area-context';
|
|
96
|
+
```
|
|
97
|
+
|
|
12
98
|
## 0.28.6
|
|
13
99
|
|
|
14
100
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@utilitywarehouse/hearth-react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.1",
|
|
4
4
|
"description": "Utility Warehouse React Native UI library",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
"@utilitywarehouse/hearth-fonts": "^0.0.4",
|
|
61
61
|
"@utilitywarehouse/hearth-react-icons": "^0.8.0",
|
|
62
62
|
"@utilitywarehouse/hearth-react-native-icons": "^0.8.0",
|
|
63
|
-
"@utilitywarehouse/hearth-
|
|
64
|
-
"@utilitywarehouse/hearth-
|
|
63
|
+
"@utilitywarehouse/hearth-tokens": "^0.2.4",
|
|
64
|
+
"@utilitywarehouse/hearth-svg-assets": "^0.5.0"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
67
|
"@gorhom/bottom-sheet": ">=5.0.0",
|
|
@@ -3,7 +3,6 @@ import { View } from 'react-native';
|
|
|
3
3
|
import { StyleSheet } from 'react-native-unistyles';
|
|
4
4
|
import { useTheme } from '../../hooks';
|
|
5
5
|
import { BodyText } from '../BodyText';
|
|
6
|
-
import { DetailText } from '../DetailText';
|
|
7
6
|
import Helper from '../Helper/Helper';
|
|
8
7
|
import { useDescriptionListContext } from './DescriptionList.context';
|
|
9
8
|
import type DescriptionListItemProps from './DescriptionListItem.props';
|
|
@@ -55,7 +54,7 @@ const DescriptionListItem = ({
|
|
|
55
54
|
</View>
|
|
56
55
|
{numericValue ? (
|
|
57
56
|
<View style={styles.descriptionWrapper}>
|
|
58
|
-
<
|
|
57
|
+
<BodyText weight="semibold">{numericValue}</BodyText>
|
|
59
58
|
</View>
|
|
60
59
|
) : null}
|
|
61
60
|
</View>
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from '@utilitywarehouse/hearth-react-native-icons';
|
|
5
5
|
import { Pressable, ViewStyle } from 'react-native';
|
|
6
6
|
import { StyleSheet } from 'react-native-unistyles';
|
|
7
|
-
import {
|
|
7
|
+
import { BodyText } from '../BodyText';
|
|
8
8
|
import ExpandableCardContent from './ExpandableCardContent';
|
|
9
9
|
import ExpandableCardHelperText from './ExpandableCardHelperText';
|
|
10
10
|
import ExpandableCardIcon from './ExpandableCardIcon';
|
|
@@ -62,10 +62,10 @@ const ExpandableCardTriggerRoot = ({
|
|
|
62
62
|
{helperText && <ExpandableCardHelperText>{helperText}</ExpandableCardHelperText>}
|
|
63
63
|
{badgePosition === 'bottom' ? badge : null}
|
|
64
64
|
</ExpandableCardContent>
|
|
65
|
-
{numericValue && (
|
|
66
|
-
<
|
|
65
|
+
{!!numericValue && (
|
|
66
|
+
<BodyText weight="semibold" style={styles.numericValue}>
|
|
67
67
|
{numericValue}
|
|
68
|
-
</
|
|
68
|
+
</BodyText>
|
|
69
69
|
)}
|
|
70
70
|
<ExpandableCardTrailingContent style={styles.chevron}>
|
|
71
71
|
<ExpandableCardTrailingIcon as={isExpanded ? ChevronUpSmallIcon : ChevronDownSmallIcon} />
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Canvas, Controls, Meta, Primary, Story } from '@storybook/addon-docs/blocks';
|
|
2
|
+
import { Box, Center, Heading, Pressable } from '../../';
|
|
3
|
+
import { BackToTopButton, UsageWrap, ViewFigmaButton } from '../../../docs/components';
|
|
2
4
|
import * as Stories from './Heading.stories';
|
|
3
|
-
import { Center, Pressable, Heading, Box } from '../../';
|
|
4
|
-
import { UsageWrap, BackToTopButton, ViewFigmaButton } from '../../../docs/components';
|
|
5
5
|
|
|
6
6
|
<Meta title="Typography / Heading" />
|
|
7
7
|
|
|
@@ -17,6 +17,7 @@ The `Heading` component gives you the ability to create headings for your screen
|
|
|
17
17
|
- [Usage](#usage)
|
|
18
18
|
- [Props](#props)
|
|
19
19
|
- [Sizes](#sizes)
|
|
20
|
+
- [Accessibility](#accessibility)
|
|
20
21
|
|
|
21
22
|
## Playground
|
|
22
23
|
|
|
@@ -60,3 +61,11 @@ const MyComponent = () => <Heading>Welcome to Utility Warehouse</Heading>;
|
|
|
60
61
|
The `Heading` component has different sizes to style the text.
|
|
61
62
|
|
|
62
63
|
<Canvas of={Stories.KitchenSink} />
|
|
64
|
+
|
|
65
|
+
## Accessibility
|
|
66
|
+
|
|
67
|
+
`Heading` sets `accessibilityRole="header"` by default, so VoiceOver and TalkBack can announce it as a heading without any extra configuration.
|
|
68
|
+
If you need to override this behavior, pass an explicit `accessibilityRole` prop (for example, `accessibilityRole="text"`) to opt out of heading semantics.
|
|
69
|
+
|
|
70
|
+
- Use `Heading` for actual screen or section titles so assistive technologies can expose the right document structure.
|
|
71
|
+
- If you need styled text without heading semantics, either override the `accessibilityRole` on `Heading` or use one of the other text components instead.
|
|
@@ -383,7 +383,7 @@ const MyComponent = () => (
|
|
|
383
383
|
trailingContent={
|
|
384
384
|
<>
|
|
385
385
|
<BodyText>-£100.00</BodyText>
|
|
386
|
-
<BodyText color="
|
|
386
|
+
<BodyText color="brand">-£100.00</BodyText>
|
|
387
387
|
</>
|
|
388
388
|
}
|
|
389
389
|
onPress={() => console.log('Transaction pressed')}
|
|
@@ -393,7 +393,7 @@ const MyComponent = () => (
|
|
|
393
393
|
helperText="Apr 4, 2024"
|
|
394
394
|
trailingContent={
|
|
395
395
|
<>
|
|
396
|
-
<BodyText color="
|
|
396
|
+
<BodyText color="affirmative">+£100.00</BodyText>
|
|
397
397
|
</>
|
|
398
398
|
}
|
|
399
399
|
onPress={() => console.log('Transaction pressed')}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Meta, StoryObj } from '@storybook/react-
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react-native';
|
|
2
2
|
import {
|
|
3
3
|
BillMediumIcon,
|
|
4
4
|
ChevronRightSmallIcon,
|
|
@@ -60,7 +60,7 @@ export default meta;
|
|
|
60
60
|
type Story = StoryObj<typeof meta>;
|
|
61
61
|
|
|
62
62
|
export const Playground: Story = {
|
|
63
|
-
render: ({ container, ...args }) => {
|
|
63
|
+
render: ({ container, ...args }: StoryObj<typeof meta.args>) => {
|
|
64
64
|
return (
|
|
65
65
|
<List {...args} container={container}>
|
|
66
66
|
{Array.from({ length: 4 }).map((_, index) => (
|
|
@@ -75,7 +75,7 @@ export const WithAction: Story = {
|
|
|
75
75
|
args: {
|
|
76
76
|
container: 'subtleWhite',
|
|
77
77
|
},
|
|
78
|
-
render: ({ container, ...args }) => (
|
|
78
|
+
render: ({ container, ...args }: StoryObj<typeof meta.args>) => (
|
|
79
79
|
<List {...args} container={container}>
|
|
80
80
|
{Array.from({ length: 4 }).map((_, index) => (
|
|
81
81
|
<ListItem key={index} heading="List item text" helperText="Supporting text" />
|
|
@@ -292,6 +292,7 @@ export const WithListAction: Story = {
|
|
|
292
292
|
heading="Manage payment methods"
|
|
293
293
|
helperText="Update your credit or debit cards"
|
|
294
294
|
onPress={() => console.log('Manage pressed')}
|
|
295
|
+
accessibilityRole="link"
|
|
295
296
|
/>
|
|
296
297
|
<ListAction heading="Contact support" onPress={() => console.log('Contact pressed')} />
|
|
297
298
|
</List>
|
|
@@ -310,7 +311,7 @@ export const WithTransactions: Story = {
|
|
|
310
311
|
trailingContent={
|
|
311
312
|
<>
|
|
312
313
|
<BodyText>-£100.00</BodyText>
|
|
313
|
-
<BodyText color="
|
|
314
|
+
<BodyText color="brand">+£1.00 CB</BodyText>
|
|
314
315
|
</>
|
|
315
316
|
}
|
|
316
317
|
onPress={() => console.log('Transaction pressed')}
|
|
@@ -320,7 +321,7 @@ export const WithTransactions: Story = {
|
|
|
320
321
|
helperText="Apr 4, 2024"
|
|
321
322
|
trailingContent={
|
|
322
323
|
<>
|
|
323
|
-
<BodyText color="
|
|
324
|
+
<BodyText color="affirmative">+£100.00</BodyText>
|
|
324
325
|
</>
|
|
325
326
|
}
|
|
326
327
|
onPress={() => console.log('Transaction pressed')}
|
|
@@ -14,7 +14,15 @@ const List = ({
|
|
|
14
14
|
invalidText,
|
|
15
15
|
...props
|
|
16
16
|
}: ListProps) => {
|
|
17
|
-
const {
|
|
17
|
+
const {
|
|
18
|
+
loading,
|
|
19
|
+
disabled,
|
|
20
|
+
container = 'none',
|
|
21
|
+
testID,
|
|
22
|
+
style,
|
|
23
|
+
accessibilityRole,
|
|
24
|
+
...rest
|
|
25
|
+
} = props;
|
|
18
26
|
|
|
19
27
|
const orderRef = useRef<string[]>([]);
|
|
20
28
|
const [firstItemId, setFirstItemId] = useState<string | undefined>(undefined);
|
|
@@ -52,7 +60,11 @@ const List = ({
|
|
|
52
60
|
styles.useVariants({ disabled });
|
|
53
61
|
return (
|
|
54
62
|
<ListContext.Provider value={value}>
|
|
55
|
-
<View
|
|
63
|
+
<View
|
|
64
|
+
{...rest}
|
|
65
|
+
accessibilityRole={accessibilityRole ?? 'list'}
|
|
66
|
+
style={[styles.container, style]}
|
|
67
|
+
>
|
|
56
68
|
{heading ? (
|
|
57
69
|
<SectionHeader
|
|
58
70
|
heading={heading}
|
|
@@ -2,7 +2,7 @@ import { ChevronRightSmallIcon } from '@utilitywarehouse/hearth-react-native-ico
|
|
|
2
2
|
import { useId, useLayoutEffect, useMemo } from 'react';
|
|
3
3
|
import { Pressable, ViewStyle } from 'react-native';
|
|
4
4
|
import { StyleSheet } from 'react-native-unistyles';
|
|
5
|
-
import {
|
|
5
|
+
import { BodyText } from '../../BodyText';
|
|
6
6
|
import { Skeleton } from '../../Skeleton';
|
|
7
7
|
import { useListContext } from '../List.context';
|
|
8
8
|
import { IListItemContext, ListItemContext } from './ListItem.context';
|
|
@@ -109,7 +109,7 @@ const ListItemRoot = ({
|
|
|
109
109
|
testID={testID}
|
|
110
110
|
style={[styles.container, props.style as ViewStyle]}
|
|
111
111
|
disabled={isDisabled}
|
|
112
|
-
accessibilityRole={onPress ? 'button' : undefined}
|
|
112
|
+
accessibilityRole={props.accessibilityRole ?? (onPress ? 'button' : undefined)}
|
|
113
113
|
>
|
|
114
114
|
{children ? (
|
|
115
115
|
children
|
|
@@ -126,7 +126,7 @@ const ListItemRoot = ({
|
|
|
126
126
|
) : null}
|
|
127
127
|
{badgePosition === 'bottom' && badge ? badge : null}
|
|
128
128
|
</ListItemContent>
|
|
129
|
-
{!!numericValue && <
|
|
129
|
+
{!!numericValue && <BodyText weight="semibold">{numericValue}</BodyText>}
|
|
130
130
|
{trailingContent ? (
|
|
131
131
|
<ListItemTrailingContent>{trailingContent}</ListItemTrailingContent>
|
|
132
132
|
) : onPress ? (
|
|
@@ -87,27 +87,28 @@ const MyComponent = () => {
|
|
|
87
87
|
|
|
88
88
|
The Modal component extends the `BottomSheetModal` component and accepts all of its props, plus the following additional props:
|
|
89
89
|
|
|
90
|
-
| Property | Type | Description
|
|
91
|
-
| ----------------------------- | ------------------------------------------------------------------ |
|
|
92
|
-
| `heading` | `string` | The heading text displayed at the top of the modal
|
|
93
|
-
| `description` | `string` | The description text displayed below the heading
|
|
94
|
-
| `showCloseButton` | `boolean` | Whether to show the close button in the top-right corner
|
|
95
|
-
| `primaryButtonText` | `string` | Text for the primary action button
|
|
96
|
-
| `secondaryButtonText` | `string` | Text for the secondary action button
|
|
97
|
-
| `onPressPrimaryButton` | `() => void` | Callback function called when the primary button is pressed
|
|
98
|
-
| `onPressSecondaryButton` | `() => void` | Callback function called when the secondary button is pressed
|
|
99
|
-
| `onPressCloseButton` | `() => void` | Callback function called when the close button is pressed
|
|
100
|
-
| `closeOnPrimaryButtonPress` | `boolean` | Whether to automatically close the modal when the primary button is pressed
|
|
101
|
-
| `closeOnSecondaryButtonPress` | `boolean` | Whether to automatically close the modal when the secondary button is pressed
|
|
102
|
-
| `onChange` | `(index: number, position: number, `<br />` type: number) => void` | Callback function called when the modal's position changes \*
|
|
103
|
-
| `loading` | `boolean` | Whether to show a loading state with spinner
|
|
104
|
-
| `loadingHeading` | `string` | The heading text to be displayed when loading is true. If not provided, the regular heading will be shown.
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
107
|
-
| `
|
|
108
|
-
| `
|
|
109
|
-
| `
|
|
110
|
-
| `
|
|
90
|
+
| Property | Type | Description | Default |
|
|
91
|
+
| ----------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | -------------- |
|
|
92
|
+
| `heading` | `string` | The heading text displayed at the top of the modal | - |
|
|
93
|
+
| `description` | `string` | The description text displayed below the heading | - |
|
|
94
|
+
| `showCloseButton` | `boolean` | Whether to show the close button in the top-right corner | `true` |
|
|
95
|
+
| `primaryButtonText` | `string` | Text for the primary action button | - |
|
|
96
|
+
| `secondaryButtonText` | `string` | Text for the secondary action button | - |
|
|
97
|
+
| `onPressPrimaryButton` | `() => void` | Callback function called when the primary button is pressed | - |
|
|
98
|
+
| `onPressSecondaryButton` | `() => void` | Callback function called when the secondary button is pressed | - |
|
|
99
|
+
| `onPressCloseButton` | `() => void` | Callback function called when the close button is pressed | - |
|
|
100
|
+
| `closeOnPrimaryButtonPress` | `boolean` | Whether to automatically close the modal when the primary button is pressed | `true` |
|
|
101
|
+
| `closeOnSecondaryButtonPress` | `boolean` | Whether to automatically close the modal when the secondary button is pressed | `true` |
|
|
102
|
+
| `onChange` | `(index: number, position: number, `<br />` type: number) => void` | Callback function called when the modal's position changes \* | - |
|
|
103
|
+
| `loading` | `boolean` | Whether to show a loading state with spinner | `false` |
|
|
104
|
+
| `loadingHeading` | `string` | The heading text to be displayed when loading is true. If not provided, the regular heading will be shown. | `'Loading...'` |
|
|
105
|
+
| `loadingDescription` | `string` | The description text to be displayed when loading is true. If not provided, the regular description will be shown. | - |
|
|
106
|
+
| `image` | `ImageProps` | Image to display in the modal (shows as centered content with text below) | - |
|
|
107
|
+
| `children` | `ReactNode` | Custom content to display in the modal body | - |
|
|
108
|
+
| `primaryButtonProps` | `Omit<ButtonWithoutChildrenProps, 'children'>` | Additional props to pass to the primary button (colorScheme defaults to 'highlight', variant to 'solid') | - |
|
|
109
|
+
| `secondaryButtonProps` | `Omit<ButtonWithoutChildrenProps, 'children'>` | Additional props to pass to the secondary button (colorScheme defaults to 'functional', variant to 'outline') | - |
|
|
110
|
+
| `closeButtonProps` | `Omit<UnstyledIconButtonProps, 'children'>` | Additional props to pass to the close button | - |
|
|
111
|
+
| `fullscreen` | `boolean` | Whether the modal should take up the full screen height | `false` |
|
|
111
112
|
|
|
112
113
|
\* use this to detect if the modal has been opened or closed, index 0 indicates open state and -1 indicates closed state
|
|
113
114
|
|
|
@@ -35,6 +35,7 @@ const Modal = ({
|
|
|
35
35
|
closeOnSecondaryButtonPress = true,
|
|
36
36
|
loading,
|
|
37
37
|
loadingHeading = 'Loading...',
|
|
38
|
+
loadingDescription,
|
|
38
39
|
fullscreen = false,
|
|
39
40
|
image,
|
|
40
41
|
primaryButtonProps,
|
|
@@ -151,7 +152,10 @@ const Modal = ({
|
|
|
151
152
|
<View
|
|
152
153
|
style={styles.loadingContainer}
|
|
153
154
|
accessible={Platform.OS === 'android' ? true : undefined}
|
|
154
|
-
accessibilityLabel={Platform.OS === 'android' ? 'Loading' : undefined}
|
|
155
|
+
accessibilityLabel={Platform.OS === 'android' ? (loadingHeading ?? 'Loading') : undefined}
|
|
156
|
+
accessibilityHint={
|
|
157
|
+
Platform.OS === 'android' && loadingDescription ? loadingDescription : undefined
|
|
158
|
+
}
|
|
155
159
|
screenReaderFocusable
|
|
156
160
|
ref={viewRef}
|
|
157
161
|
>
|
|
@@ -159,6 +163,7 @@ const Modal = ({
|
|
|
159
163
|
<Heading size="lg" textAlign="center">
|
|
160
164
|
{loadingHeading}
|
|
161
165
|
</Heading>
|
|
166
|
+
{loadingDescription ? <BodyText textAlign="center">{loadingDescription}</BodyText> : null}
|
|
162
167
|
</View>
|
|
163
168
|
) : (
|
|
164
169
|
<View
|
|
@@ -135,29 +135,30 @@ const styles = StyleSheet.create({
|
|
|
135
135
|
|
|
136
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
137
|
|
|
138
|
-
| Property | Type | Description
|
|
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.
|
|
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.
|
|
152
|
-
| `loadingHeading` | `string` | Heading text shown while `loading` is true.
|
|
153
|
-
| `
|
|
154
|
-
| `
|
|
155
|
-
| `
|
|
156
|
-
| `
|
|
157
|
-
| `
|
|
158
|
-
| `
|
|
159
|
-
| `
|
|
160
|
-
| `
|
|
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
|
+
| `loadingDescription` | `string` | Supporting text shown below the heading while `loading` is true. | - |
|
|
154
|
+
| `image` | `ReactNode` | Optional image or illustration shown above the text content. | - |
|
|
155
|
+
| `children` | `ReactNode` | Content rendered inside the modal body. | - |
|
|
156
|
+
| `stickyFooter` | `boolean` | Keeps action buttons pinned to the bottom instead of flowing with the content. | `true` |
|
|
157
|
+
| `background` | `'default' /\| 'brand'` | Switches between the default surface background and the brand background treatment. | `'default'` |
|
|
158
|
+
| `scrollable` | `boolean` | Wraps the content area in a `ScrollView`. Set this to `false` for custom layouts that should not scroll. | `true` |
|
|
159
|
+
| `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'` |
|
|
160
|
+
| `useSafeAreaInsets` | `boolean` | Whether to apply safe area insets as padding within the component. This is enabled by default to fix full-screen presentation padding but can be disabled if you want to manage insets yourself. | `true` |
|
|
161
|
+
| `scrollViewProps` | `ScrollViewProps` | Extra props forwarded to the `ScrollView` wrapping the modal content when `scrollable` is true. | - |
|
|
161
162
|
|
|
162
163
|
## Accessibility
|
|
163
164
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Ref } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { ScrollViewProps } from 'react-native';
|
|
3
3
|
import { ModalCommonProps } from '../Modal/Modal.shared.types';
|
|
4
4
|
|
|
5
5
|
export interface NavModalRef {
|
|
@@ -16,8 +16,8 @@ interface NavModalProps extends ModalCommonProps {
|
|
|
16
16
|
| 'transparentModal'
|
|
17
17
|
| 'containedModal'
|
|
18
18
|
| 'containedTransparentModal';
|
|
19
|
-
|
|
20
|
-
scrollViewProps?: Omit<
|
|
19
|
+
useSafeAreaInsets?: boolean;
|
|
20
|
+
scrollViewProps?: Omit<ScrollViewProps, 'children'>;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export default NavModalProps;
|
|
@@ -8,7 +8,6 @@ import Animated, {
|
|
|
8
8
|
withDelay,
|
|
9
9
|
withTiming,
|
|
10
10
|
} from 'react-native-reanimated';
|
|
11
|
-
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
12
11
|
import { StyleSheet } from 'react-native-unistyles';
|
|
13
12
|
import { useTheme } from '../../hooks';
|
|
14
13
|
import { hexWithOpacity } from '../../utils';
|
|
@@ -32,6 +31,7 @@ const NavModal = ({
|
|
|
32
31
|
onPressSecondaryButton,
|
|
33
32
|
loading,
|
|
34
33
|
loadingHeading = 'Loading...',
|
|
34
|
+
loadingDescription,
|
|
35
35
|
image,
|
|
36
36
|
primaryButtonProps,
|
|
37
37
|
secondaryButtonProps,
|
|
@@ -41,7 +41,7 @@ const NavModal = ({
|
|
|
41
41
|
scrollable = true,
|
|
42
42
|
presentation = 'modal',
|
|
43
43
|
scrollViewProps,
|
|
44
|
-
|
|
44
|
+
useSafeAreaInsets = true,
|
|
45
45
|
...props
|
|
46
46
|
}: NavModalProps) => {
|
|
47
47
|
const theme = useTheme();
|
|
@@ -116,6 +116,9 @@ const NavModal = ({
|
|
|
116
116
|
loading,
|
|
117
117
|
background: isBrandBackground ? 'brand' : 'primary',
|
|
118
118
|
presentation: isFullScreenPresentation ? 'fullScreen' : 'modal',
|
|
119
|
+
useSafeAreaInsets,
|
|
120
|
+
usesSheetPresentation,
|
|
121
|
+
stickyFooter,
|
|
119
122
|
});
|
|
120
123
|
|
|
121
124
|
const footer = useMemo(
|
|
@@ -162,13 +165,21 @@ const NavModal = ({
|
|
|
162
165
|
<View
|
|
163
166
|
style={styles.loadingContainer}
|
|
164
167
|
accessible={Platform.OS === 'android' ? true : undefined}
|
|
165
|
-
accessibilityLabel={Platform.OS === 'android' ? 'Loading' : undefined}
|
|
168
|
+
accessibilityLabel={Platform.OS === 'android' ? (loadingHeading ?? 'Loading') : undefined}
|
|
169
|
+
accessibilityHint={
|
|
170
|
+
Platform.OS === 'android' && loadingDescription ? loadingDescription : undefined
|
|
171
|
+
}
|
|
166
172
|
screenReaderFocusable
|
|
167
173
|
>
|
|
168
174
|
<Spinner size="lg" color={isBrandBackground ? theme.color.icon.inverted : undefined} />
|
|
169
175
|
<Heading size="lg" textAlign="center" inverted={isBrandBackground}>
|
|
170
176
|
{loadingHeading}
|
|
171
177
|
</Heading>
|
|
178
|
+
{loadingDescription ? (
|
|
179
|
+
<BodyText size="md" textAlign="center" inverted={isBrandBackground}>
|
|
180
|
+
{loadingDescription}
|
|
181
|
+
</BodyText>
|
|
182
|
+
) : null}
|
|
172
183
|
</View>
|
|
173
184
|
) : (
|
|
174
185
|
<View
|
|
@@ -249,19 +260,8 @@ const NavModal = ({
|
|
|
249
260
|
</>
|
|
250
261
|
);
|
|
251
262
|
|
|
252
|
-
const { style: safeAreaViewStyle, ...restSafeAreaViewProps } = safeAreaViewProps ?? {};
|
|
253
|
-
|
|
254
263
|
return (
|
|
255
|
-
<
|
|
256
|
-
style={[
|
|
257
|
-
{
|
|
258
|
-
flex: 1,
|
|
259
|
-
backgroundColor: theme.color.background[isBrandBackground ? 'brand' : 'secondary'],
|
|
260
|
-
},
|
|
261
|
-
safeAreaViewStyle,
|
|
262
|
-
]}
|
|
263
|
-
{...restSafeAreaViewProps}
|
|
264
|
-
>
|
|
264
|
+
<View style={styles.root}>
|
|
265
265
|
{Platform.OS === 'android' && usesSheetPresentation ? (
|
|
266
266
|
<Animated.View style={[styles.androidContainer, animatedBackgroundStyle]}>
|
|
267
267
|
<Animated.View style={[styles.pretendContent, animatedPretendContentStyle]} />
|
|
@@ -276,11 +276,56 @@ const NavModal = ({
|
|
|
276
276
|
>
|
|
277
277
|
<View style={styles.inNavModalContent}>{content}</View>
|
|
278
278
|
</Animated.View>
|
|
279
|
-
</
|
|
279
|
+
</View>
|
|
280
280
|
);
|
|
281
281
|
};
|
|
282
282
|
|
|
283
283
|
const styles = StyleSheet.create((theme, rt) => ({
|
|
284
|
+
root: {
|
|
285
|
+
flex: 1,
|
|
286
|
+
variants: {
|
|
287
|
+
background: {
|
|
288
|
+
brand: {
|
|
289
|
+
backgroundColor: theme.color.background.brand,
|
|
290
|
+
},
|
|
291
|
+
primary: {
|
|
292
|
+
backgroundColor: theme.color.background.secondary,
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
usesSheetPresentation: {
|
|
296
|
+
true: {},
|
|
297
|
+
false: {},
|
|
298
|
+
},
|
|
299
|
+
useSafeAreaInsets: {
|
|
300
|
+
true: {},
|
|
301
|
+
false: {},
|
|
302
|
+
},
|
|
303
|
+
stickyFooter: {
|
|
304
|
+
true: {},
|
|
305
|
+
false: {},
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
compoundVariants: [
|
|
309
|
+
{
|
|
310
|
+
usesSheetPresentation: false,
|
|
311
|
+
useSafeAreaInsets: true,
|
|
312
|
+
styles: {
|
|
313
|
+
paddingTop: rt.insets.top,
|
|
314
|
+
paddingBottom: Platform.OS === 'ios' ? rt.insets.bottom : 0,
|
|
315
|
+
paddingLeft: rt.insets.left,
|
|
316
|
+
paddingRight: rt.insets.right,
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
usesSheetPresentation: true,
|
|
321
|
+
useSafeAreaInsets: true,
|
|
322
|
+
stickyFooter: true,
|
|
323
|
+
styles: {
|
|
324
|
+
paddingBottom: Platform.OS === 'ios' ? rt.insets.bottom : 0,
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
},
|
|
284
329
|
container: {
|
|
285
330
|
flex: 1,
|
|
286
331
|
gap: theme.components.modal.gap,
|