@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.
Files changed (32) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-lint.log +13 -13
  3. package/CHANGELOG.md +67 -0
  4. package/build/components/DescriptionList/DescriptionListItem.js +1 -2
  5. package/build/components/ExpandableCard/ExpandableCardTriggerRoot.js +2 -2
  6. package/build/components/Heading/Heading.js +1 -1
  7. package/build/components/List/List.js +2 -2
  8. package/build/components/List/ListAction/ListAction.js +1 -1
  9. package/build/components/List/ListItem/ListItemRoot.js +2 -2
  10. package/build/components/Modal/Modal.d.ts +1 -1
  11. package/build/components/Modal/Modal.js +2 -2
  12. package/build/components/Modal/Modal.shared.types.d.ts +1 -0
  13. package/build/components/NavModal/NavModal.d.ts +1 -1
  14. package/build/components/NavModal/NavModal.js +51 -11
  15. package/build/components/NavModal/NavModal.props.d.ts +3 -3
  16. package/docs/changelog.mdx +86 -0
  17. package/package.json +3 -3
  18. package/src/components/DescriptionList/DescriptionListItem.tsx +1 -2
  19. package/src/components/ExpandableCard/ExpandableCardTriggerRoot.tsx +4 -4
  20. package/src/components/Heading/Heading.docs.mdx +12 -3
  21. package/src/components/Heading/Heading.tsx +1 -0
  22. package/src/components/List/List.docs.mdx +2 -2
  23. package/src/components/List/List.stories.tsx +6 -5
  24. package/src/components/List/List.tsx +14 -2
  25. package/src/components/List/ListAction/ListAction.tsx +1 -0
  26. package/src/components/List/ListItem/ListItemRoot.tsx +3 -3
  27. package/src/components/Modal/Modal.docs.mdx +22 -21
  28. package/src/components/Modal/Modal.shared.types.ts +1 -0
  29. package/src/components/Modal/Modal.tsx +6 -1
  30. package/src/components/NavModal/NavModal.docs.mdx +24 -23
  31. package/src/components/NavModal/NavModal.props.ts +3 -3
  32. package/src/components/NavModal/NavModal.tsx +61 -16
@@ -1,4 +1,4 @@
1
1
 
2
- > @utilitywarehouse/hearth-react-native@0.29.2 build /home/runner/work/hearth/hearth/packages/react-native
2
+ > @utilitywarehouse/hearth-react-native@0.30.1 build /home/runner/work/hearth/hearth/packages/react-native
3
3
  > tsc
4
4
 
@@ -1,5 +1,5 @@
1
1
 
2
- > @utilitywarehouse/hearth-react-native@0.29.2 lint /home/runner/work/hearth/hearth/packages/react-native
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 | Time (ms) | Relative
55
- :----------------------------------------|----------:|--------:
56
- @typescript-eslint/no-unused-vars | 1520.806 | 60.5%
57
- react-hooks/exhaustive-deps | 119.554 | 4.8%
58
- no-global-assign | 82.827 | 3.3%
59
- react-hooks/rules-of-hooks | 73.620 | 2.9%
60
- no-loss-of-precision | 46.636 | 1.9%
61
- no-unexpected-multiline | 38.241 | 1.5%
62
- no-misleading-character-class | 37.413 | 1.5%
63
- @typescript-eslint/no-unused-expressions | 31.682 | 1.3%
64
- no-useless-escape | 28.666 | 1.1%
65
- @typescript-eslint/ban-ts-comment | 27.948 | 1.1%
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(DetailText, { size: "lg", children: numericValue }) })) : null] }), trailingContent ? trailingContent : null] }));
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 { DetailText } from '../DetailText';
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(DetailText, { size: "lg", style: styles.numericValue, children: numericValue })), _jsx(ExpandableCardTrailingContent, { style: styles.chevron, children: _jsx(ExpandableCardTrailingIcon, { as: isExpanded ? ChevronUpSmallIcon : ChevronDownSmallIcon }) })] }));
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 { DetailText } from '../../DetailText';
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(DetailText, { size: "lg", children: numericValue }), trailingContent ? (_jsx(ListItemTrailingContent, { children: trailingContent })) : onPress ? (_jsx(ListItemTrailingContent, { style: styles.centeredTrailingIcon, children: _jsx(ListItemTrailingIcon, { as: ChevronRightSmallIcon }) })) : null] })) }) }));
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
  };
@@ -8,6 +8,7 @@ export interface ModalCommonProps {
8
8
  showCloseButton?: boolean;
9
9
  heading?: string;
10
10
  loadingHeading?: string;
11
+ loadingDescription?: string;
11
12
  description?: string;
12
13
  stickyFooter?: boolean;
13
14
  children?: ViewProps['children'];
@@ -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, safeAreaViewProps, ...props }: NavModalProps) => import("react/jsx-runtime").JSX.Element;
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, safeAreaViewProps, ...props }) => {
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
- const { style: safeAreaViewStyle, ...restSafeAreaViewProps } = safeAreaViewProps ?? {};
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 { SafeAreaViewProps } from 'react-native-safe-area-context';
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
- safeAreaViewProps?: Omit<SafeAreaViewProps, 'children'>;
13
- scrollViewProps?: Omit<SafeAreaViewProps, 'children'>;
12
+ useSafeAreaInsets?: boolean;
13
+ scrollViewProps?: Omit<ScrollViewProps, 'children'>;
14
14
  }
15
15
  export default NavModalProps;
@@ -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.29.2",
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-svg-assets": "^0.5.0",
64
- "@utilitywarehouse/hearth-tokens": "^0.2.4"
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
- <DetailText size="lg">{numericValue}</DetailText>
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 { DetailText } from '../DetailText';
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
- <DetailText size="lg" style={styles.numericValue}>
65
+ {!!numericValue && (
66
+ <BodyText weight="semibold" style={styles.numericValue}>
67
67
  {numericValue}
68
- </DetailText>
68
+ </BodyText>
69
69
  )}
70
70
  <ExpandableCardTrailingContent style={styles.chevron}>
71
71
  <ExpandableCardTrailingIcon as={isExpanded ? ChevronUpSmallIcon : ChevronDownSmallIcon} />
@@ -1,7 +1,7 @@
1
- import { Meta, Canvas, Story, Controls, Primary } from '@storybook/addon-docs/blocks';
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.
@@ -33,6 +33,7 @@ const Heading = ({
33
33
 
34
34
  return (
35
35
  <Text
36
+ accessibilityRole="header"
36
37
  {...remainingProps}
37
38
  {...(truncated
38
39
  ? {
@@ -383,7 +383,7 @@ const MyComponent = () => (
383
383
  trailingContent={
384
384
  <>
385
385
  <BodyText>-£100.00</BodyText>
386
- <BodyText color="textBrand">-£100.00</BodyText>
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="textAffirmative">+£100.00</BodyText>
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-vite';
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="textBrand">+£1.00 CB</BodyText>
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="textAffirmative">+£100.00</BodyText>
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 { loading, disabled, container = 'none', testID, style, ...rest } = props;
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 {...rest} style={[styles.container, style]}>
63
+ <View
64
+ {...rest}
65
+ accessibilityRole={accessibilityRole ?? 'list'}
66
+ style={[styles.container, style]}
67
+ >
56
68
  {heading ? (
57
69
  <SectionHeader
58
70
  heading={heading}
@@ -62,6 +62,7 @@ const ListActionRoot = ({
62
62
  return (
63
63
  <Pressable
64
64
  {...props}
65
+ accessibilityRole={props.accessibilityRole ?? 'button'}
65
66
  testID={testID}
66
67
  style={[styles.container, props.style as ViewStyle]}
67
68
  disabled={isDisabled || !onPress}
@@ -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 { DetailText } from '../../DetailText';
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 && <DetailText size="lg">{numericValue}</DetailText>}
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 | 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
- | `image` | `ImageProps` | Image to display in the modal (shows as centered content with text below) | - |
106
- | `children` | `ReactNode` | Custom content to display in the modal body | - |
107
- | `primaryButtonProps` | `Omit<ButtonWithoutChildrenProps, 'children'>` | Additional props to pass to the primary button (colorScheme defaults to 'highlight', variant to 'solid') | - |
108
- | `secondaryButtonProps` | `Omit<ButtonWithoutChildrenProps, 'children'>` | Additional props to pass to the secondary button (colorScheme defaults to 'functional', variant to 'outline') | - |
109
- | `closeButtonProps` | `Omit<UnstyledIconButtonProps, 'children'>` | Additional props to pass to the close button | - |
110
- | `fullscreen` | `boolean` | Whether the modal should take up the full screen height | `false` |
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
 
@@ -9,6 +9,7 @@ export interface ModalCommonProps {
9
9
  showCloseButton?: boolean;
10
10
  heading?: string;
11
11
  loadingHeading?: string;
12
+ loadingDescription?: string;
12
13
  description?: string;
13
14
  stickyFooter?: boolean;
14
15
  children?: ViewProps['children'];
@@ -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 | Default |
139
- | ------------------------ | ---------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | -------------- |
140
- | `heading` | `string` | Heading text shown at the top of the modal when no `image` is provided. | - |
141
- | `description` | `string` | Supporting text shown below the heading when no `image` is provided. | - |
142
- | `showCloseButton` | `boolean` | Whether to render the close button in the top-right corner. | `true` |
143
- | `primaryButtonText` | `string` | Label for the primary action button. | - |
144
- | `secondaryButtonText` | `string` | Label for the secondary action button. | - |
145
- | `onPressPrimaryButton` | `() => void` | Called when the primary action button is pressed. | - |
146
- | `onPressSecondaryButton` | `() => void` | Called when the secondary action button is pressed. | - |
147
- | `onPressCloseButton` | `() => void` | Called when the close button is pressed. | - |
148
- | `primaryButtonProps` | `Omit<ButtonWithoutChildrenProps, 'children'>` | Extra props forwarded to the primary button. | - |
149
- | `secondaryButtonProps` | `Omit<ButtonWithoutChildrenProps, 'children'>` | Extra props forwarded to the secondary button. | - |
150
- | `closeButtonProps` | `Omit<UnstyledIconButtonProps, 'children'>` | Extra props forwarded to the close button. | - |
151
- | `loading` | `boolean` | Replaces the content with a loading state and spinner. | `false` |
152
- | `loadingHeading` | `string` | Heading text shown while `loading` is true. | `'Loading...'` |
153
- | `image` | `ReactNode` | Optional image or illustration shown above the text content. | - |
154
- | `children` | `ReactNode` | Content rendered inside the modal body. | - |
155
- | `stickyFooter` | `boolean` | Keeps action buttons pinned to the bottom instead of flowing with the content. | `true` |
156
- | `background` | `'default' /\| 'brand'` | Switches between the default surface background and the brand background treatment. | `'default'` |
157
- | `scrollable` | `boolean` | Wraps the content area in a `ScrollView`. Set this to `false` for custom layouts that should not scroll. | `true` |
158
- | `presentation` | `'modal' \| 'fullScreenModal' \| 'transparentModal' `<br />` \| 'containedModal' \| 'containedTransparentModal'` | Matches the React Navigation screen presentation. `fullScreenModal` uses the full-screen layout; the other values use the sheet-style layout. | `'modal'` |
159
- | `safeAreaViewProps` | `SafeAreaViewProps` | Extra props forwarded to the `SafeAreaView` wrapping the modal content. | - |
160
- | `scrollViewProps` | `ScrollViewProps` | Extra props forwarded to the `ScrollView` wrapping the modal content when `scrollable` is true. | - |
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 { SafeAreaViewProps } from 'react-native-safe-area-context';
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
- safeAreaViewProps?: Omit<SafeAreaViewProps, 'children'>;
20
- scrollViewProps?: Omit<SafeAreaViewProps, 'children'>;
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
- safeAreaViewProps,
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
- <SafeAreaView
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
- </SafeAreaView>
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,