@utilitywarehouse/hearth-react-native 0.34.4 → 0.34.5
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/CHANGELOG.md +31 -0
- package/CLAUDE.md +109 -0
- package/build/components/Modal/Modal.js +3 -1
- package/build/components/Modal/Modal.shared.types.d.ts +1 -1
- package/build/components/Modal/Modal.web.js +3 -1
- package/build/components/NavModal/NavModal.js +3 -1
- package/docs/changelog.mdx +31 -0
- package/package.json +3 -3
- package/src/components/Modal/Modal.docs.mdx +52 -25
- package/src/components/Modal/Modal.shared.types.ts +1 -1
- package/src/components/Modal/Modal.tsx +17 -5
- package/src/components/Modal/Modal.web.tsx +17 -5
- package/src/components/NavModal/NavModal.docs.mdx +17 -2
- package/src/components/NavModal/NavModal.stories.tsx +40 -0
- package/src/components/NavModal/NavModal.tsx +18 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# @utilitywarehouse/hearth-react-native
|
|
2
2
|
|
|
3
|
+
## 0.34.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#1384](https://github.com/utilitywarehouse/hearth/pull/1384) [`be839e2`](https://github.com/utilitywarehouse/hearth/commit/be839e2ca46f079a0fe867f976044f4f31cca551) Thanks [@jordmccord](https://github.com/jordmccord)! - 🌟 [FEATURE]: `NavModal` and `Modal` `description` prop accepts JSX
|
|
8
|
+
|
|
9
|
+
`description` now accepts `ReactNode` in addition to a string, so you can pass
|
|
10
|
+
custom content such as links or styled text alongside the default text
|
|
11
|
+
styling.
|
|
12
|
+
|
|
13
|
+
**Components affected**:
|
|
14
|
+
|
|
15
|
+
- `NavModal`
|
|
16
|
+
- `Modal`
|
|
17
|
+
|
|
18
|
+
**Developer changes**:
|
|
19
|
+
|
|
20
|
+
No changes required for existing usage. To pass custom content, provide JSX
|
|
21
|
+
instead of a string:
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
<Modal
|
|
25
|
+
heading="Update available"
|
|
26
|
+
description={
|
|
27
|
+
<BodyText>
|
|
28
|
+
Read the <InlineLink onPress={() => {}}>release notes</InlineLink> before updating.
|
|
29
|
+
</BodyText>
|
|
30
|
+
}
|
|
31
|
+
/>
|
|
32
|
+
```
|
|
33
|
+
|
|
3
34
|
## 0.34.4
|
|
4
35
|
|
|
5
36
|
### Patch Changes
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
This file covers `packages/react-native` — `@utilitywarehouse/hearth-react-native`, the React Native
|
|
6
|
+
component library. See the [repo-root CLAUDE.md](../../CLAUDE.md) for monorepo-wide setup and commands.
|
|
7
|
+
|
|
8
|
+
## Commands
|
|
9
|
+
|
|
10
|
+
Run from this directory (`packages/react-native`), or via the root scripts shown for Turbo-filtered variants.
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
pnpm dev # Storybook (React Native Web) on :6006 — primary dev loop for this package
|
|
14
|
+
pnpm dev:docs # Storybook docs-only mode on :6002
|
|
15
|
+
|
|
16
|
+
pnpm build # tsc — compiles src/ to build/
|
|
17
|
+
pnpm watch # tsc --watch
|
|
18
|
+
|
|
19
|
+
pnpm test # vitest run --config vitest.unit.config.ts — unit tests (src/**/*.test.{ts,tsx})
|
|
20
|
+
pnpm test:storybook # vitest run --project storybook — runs stories as browser tests via Playwright
|
|
21
|
+
|
|
22
|
+
pnpm lint # eslint .
|
|
23
|
+
pnpm lint:fix # eslint --fix .
|
|
24
|
+
|
|
25
|
+
pnpm copyTokens # vendors CSS tokens from packages/tokens into src/tokens — run after changing hearth-tokens
|
|
26
|
+
pnpm generateColours # regenerates legacy colour token files
|
|
27
|
+
|
|
28
|
+
pnpm figma:create # scaffold a Code Connect entry
|
|
29
|
+
pnpm figma:publish # publish Code Connect mappings
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Run a single unit test file: `pnpm test src/hooks/useFormFieldAccessibility.test.tsx`.
|
|
33
|
+
|
|
34
|
+
Root-level equivalents (from repo root):
|
|
35
|
+
```sh
|
|
36
|
+
pnpm dev:react-native # turbo run @utilitywarehouse/hearth-react-native#dev
|
|
37
|
+
pnpm test:react-native # unit tests + storybook tests for this package
|
|
38
|
+
pnpm build:react-native # turbo run build:storybook, filtered to this package
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Architecture
|
|
42
|
+
|
|
43
|
+
### Styling: Unistyles, not StyleSheet-from-react-native
|
|
44
|
+
|
|
45
|
+
Every component is styled with `react-native-unistyles`'s `StyleSheet.create(theme => ({...}))`, re-exported
|
|
46
|
+
from `src/core`. Styles are defined as **variants** (e.g. `size`, `colorScheme`, `variant`, `disabled`) plus
|
|
47
|
+
`compoundVariants` for combinations that need distinct treatment, and applied at render time via
|
|
48
|
+
`styles.useVariants({...})`. See [`Button/ButtonRoot.tsx`](src/components/Button/ButtonRoot.tsx) for the
|
|
49
|
+
canonical example — colour/variant/state combinations are resolved entirely through compound variants rather
|
|
50
|
+
than conditional logic in the component body. `_web` blocks inside a style object hold web-only pseudo-class
|
|
51
|
+
styles (`_hover`, `_focus-visible`, `_active`) since Unistyles targets both native and `react-native-web`.
|
|
52
|
+
|
|
53
|
+
Unistyles is configured once in `src/core/index.ts` (`StyleSheet.configure({ breakpoints, themes, settings })`),
|
|
54
|
+
which is why components never configure themes themselves — they just consume `theme` inside `StyleSheet.create`.
|
|
55
|
+
|
|
56
|
+
### Token pipeline: tokens package → legacy + generated → theme
|
|
57
|
+
|
|
58
|
+
- `src/tokens/` is **generated, not hand-edited** ("Do not edit directly" — see file headers). It's populated
|
|
59
|
+
by `pnpm copyTokens` from `@utilitywarehouse/hearth-tokens` (see root CLAUDE.md: tokens are vendored, not a
|
|
60
|
+
runtime dependency).
|
|
61
|
+
- `src/legacyTokens/` holds older colour tokens (`colors`, `colorsCommon`, `colorsDark`) still consumed by
|
|
62
|
+
`src/core/themes.ts` alongside the generated tokens — check both when tracing a colour value.
|
|
63
|
+
- `src/core/themes.ts` assembles the final `light`/`dark` theme objects consumed via `theme.*` inside
|
|
64
|
+
components, merging `tokens/*` (space, color, typography, shadow, border, `components.*` for
|
|
65
|
+
per-component tokens) with responsive breakpoint-aware values (`base`/`md`/`lg` per breakpoint).
|
|
66
|
+
- Component-specific tokens live under `theme.components.<componentName>` (e.g. `theme.components.button.md.paddingVertical`)
|
|
67
|
+
— check `src/tokens/components/{light,dark}` before hardcoding a dimension in a component.
|
|
68
|
+
|
|
69
|
+
### Component folder convention
|
|
70
|
+
|
|
71
|
+
Each component under `src/components/<Component>/` follows a fixed file set (see the
|
|
72
|
+
`react-native-component-addition` skill for full detail when adding a new one):
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
<Component>.tsx / <Component>Root.tsx / <Component>Xyz.tsx # implementation, split by subcomponent
|
|
76
|
+
<Component>.props.ts # public prop types
|
|
77
|
+
<Component>.context.ts # React context for compound components (e.g. ButtonContext shares state with ButtonIcon/ButtonText)
|
|
78
|
+
<Component>.stories.tsx # Storybook stories
|
|
79
|
+
<Component>.docs.mdx # Storybook docs page
|
|
80
|
+
<Component>.figma.tsx # Figma Code Connect mapping (excluded from eslint and turbo build inputs)
|
|
81
|
+
index.ts # public exports for the folder
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Compound components (Button, Accordion, Card, etc.) share state via a `<Component>Context` rather than prop
|
|
85
|
+
drilling — check for a `.context.ts` file before assuming a subcomponent is standalone.
|
|
86
|
+
|
|
87
|
+
`src/index.ts` re-exports only `core`, `hooks`, and `components` — anything not re-exported there (e.g.
|
|
88
|
+
internal `utils/`) is not part of the public API.
|
|
89
|
+
|
|
90
|
+
### Storybook / testing split
|
|
91
|
+
|
|
92
|
+
There are two distinct test configs, and they check different things:
|
|
93
|
+
- `vitest.unit.config.ts` — plain unit tests (`src/**/*.test.ts(x)`), Node environment, e.g.
|
|
94
|
+
[`useFormFieldAccessibility.test.tsx`](src/hooks/useFormFieldAccessibility.test.tsx).
|
|
95
|
+
- `vitest.config.js` — runs `*.stories.tsx` themselves as browser tests via `@storybook/addon-vitest` +
|
|
96
|
+
Playwright/Chromium, driven by `.storybook/` config. This is how story-level interaction tests are verified,
|
|
97
|
+
not a separate manual QA step.
|
|
98
|
+
|
|
99
|
+
Storybook itself targets `@storybook/react-native-web-vite`, so stories run against `react-native-web`, not a
|
|
100
|
+
native simulator — this is also why `_web`-scoped Unistyles blocks matter for how components look in Storybook.
|
|
101
|
+
|
|
102
|
+
### Peer dependency surface
|
|
103
|
+
|
|
104
|
+
This package is built on and requires consumers to install `react-native-unistyles`, `react-native-svg`,
|
|
105
|
+
`react-native-reanimated` (+ `react-native-worklets`), `react-native-gesture-handler`,
|
|
106
|
+
`react-native-safe-area-context`, `@gorhom/bottom-sheet`, and `@utilitywarehouse/hearth-react-native-icons`
|
|
107
|
+
(see `docs/introduction.mdx` for consumer-side setup — Babel plugin, font linking, Jest mocks). When changing a
|
|
108
|
+
component's animation or gesture behaviour, assume these are the only native modules available; don't add a new
|
|
109
|
+
native dependency without checking the peerDependencies list in `package.json`.
|
|
@@ -70,6 +70,8 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
|
|
|
70
70
|
const hasSecondaryButton = !!(onPressSecondaryButton && secondaryButtonText);
|
|
71
71
|
const hasFooter = !!footer || hasPrimaryButton || hasSecondaryButton;
|
|
72
72
|
const shouldShowFooter = !loading && hasFooter;
|
|
73
|
+
const descriptionIsText = typeof description === 'string' || typeof description === 'number';
|
|
74
|
+
const hasDescription = description !== undefined && description !== null;
|
|
73
75
|
styles.useVariants({
|
|
74
76
|
loading,
|
|
75
77
|
noButtons: !shouldShowFooter,
|
|
@@ -88,7 +90,7 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
|
|
|
88
90
|
secondaryButtonProps,
|
|
89
91
|
secondaryButtonText,
|
|
90
92
|
]);
|
|
91
|
-
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,
|
|
93
|
+
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, hasDescription && !image ? (descriptionIsText ? (_jsx(BodyText, { accessible: true, children: description })) : (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, hasDescription ? (descriptionIsText ? (_jsx(BodyText, { textAlign: "center", accessible: true, children: description })) : (description)) : null] })] })) : null, children, !stickyFooter && shouldShowFooter ? (_jsx(View, { style: footerStyle, children: footerContent })) : null] })) }));
|
|
92
94
|
const renderFooter = useCallback((bottomSheetFooterProps) => (_jsx(BottomSheetFooter, { ...bottomSheetFooterProps, children: _jsx(View, { onLayout: handleStickyFooterLayout, style: [styles.footerWrap, footerStyle], children: footerContent }) })), [footerContent, footerStyle, handleStickyFooterLayout]);
|
|
93
95
|
return (_jsxs(_Fragment, { children: [stickyFooter && shouldShowFooter && stickyFooterHeight === 0 ? (_jsx(View, { accessible: false, importantForAccessibility: "no-hide-descendants", pointerEvents: "none", style: styles.footerMeasurementContainer, children: _jsx(View, { onLayout: handleStickyFooterLayout, style: [styles.footerWrap, footerStyle], children: footerContent }) })) : null, _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 && shouldShowFooter ? renderFooter : undefined, ...props, onChange: handleChange, children: [loading ? _jsx(View, { style: styles.loadingTop }) : null, _jsx(BottomSheetScrollView, { contentContainerStyle: [
|
|
94
96
|
styles.scrollView,
|
|
@@ -9,7 +9,7 @@ export interface ModalCommonBaseProps {
|
|
|
9
9
|
heading?: string;
|
|
10
10
|
loadingHeading?: string;
|
|
11
11
|
loadingDescription?: string;
|
|
12
|
-
description?:
|
|
12
|
+
description?: ReactNode;
|
|
13
13
|
stickyFooter?: boolean;
|
|
14
14
|
children?: ViewProps['children'];
|
|
15
15
|
onPressCloseButton?: () => void;
|
|
@@ -59,8 +59,10 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
|
|
|
59
59
|
const hasPrimaryButton = !!(onPressPrimaryButton && primaryButtonText);
|
|
60
60
|
const hasSecondaryButton = !!(onPressSecondaryButton && secondaryButtonText);
|
|
61
61
|
const hasFooter = !!footer || hasPrimaryButton || hasSecondaryButton;
|
|
62
|
+
const descriptionIsText = typeof description === 'string' || typeof description === 'number';
|
|
63
|
+
const hasDescription = description !== undefined && description !== null;
|
|
62
64
|
const footerContent = footer ?? (_jsxs(View, { style: styles.footer, children: [hasPrimaryButton ? (_jsx(Button, { onPress: handlePrimaryButtonPress, text: primaryButtonText, ...primaryButtonProps, variant: primaryButtonProps?.variant ?? 'solid', colorScheme: primaryButtonProps?.colorScheme ?? 'highlight' })) : null, hasSecondaryButton ? (_jsx(Button, { onPress: handleSecondaryButtonPress, text: secondaryButtonText, ...secondaryButtonProps, variant: secondaryButtonProps?.variant ?? 'outline', colorScheme: secondaryButtonProps?.colorScheme ?? 'functional' })) : null] }));
|
|
63
|
-
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,
|
|
65
|
+
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, hasDescription && !image ? (descriptionIsText ? (_jsx(BodyText, { accessible: true, children: description })) : (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, hasDescription ? (descriptionIsText ? (_jsx(BodyText, { textAlign: "center", accessible: true, children: description })) : (description)) : null] })] })) : null, children, hasFooter ? _jsx(View, { style: footerStyle, children: footerContent }) : null] })) }));
|
|
64
66
|
return (_jsx(BottomSheetModal, { ref: bottomSheetModalRef, enableDynamicSizing: true, snapPoints: image || fullscreen ? ['90%'] : props.snapPoints, showHandle: typeof loading !== 'undefined' && loading ? false : props.showHandle, accessible: false, ...props, onChange: handleChange, children: _jsx(BottomSheetScrollView, { contentContainerStyle: styles.container, ref: scrollViewRef, children: content }) }));
|
|
65
67
|
};
|
|
66
68
|
const styles = StyleSheet.create(theme => ({
|
|
@@ -63,6 +63,8 @@ const NavModal = ({ ref, children, heading, description, showCloseButton = true,
|
|
|
63
63
|
const hasPrimaryButton = !!(onPressPrimaryButton && primaryButtonText);
|
|
64
64
|
const hasSecondaryButton = !!(onPressSecondaryButton && secondaryButtonText);
|
|
65
65
|
const hasFooter = !!footer || hasPrimaryButton || hasSecondaryButton;
|
|
66
|
+
const descriptionIsText = typeof description === 'string' || typeof description === 'number';
|
|
67
|
+
const hasDescription = description !== undefined && description !== null;
|
|
66
68
|
styles.useVariants({
|
|
67
69
|
loading,
|
|
68
70
|
background: isBrandBackground ? 'brand' : 'primary',
|
|
@@ -83,7 +85,7 @@ const NavModal = ({ ref, children, heading, description, showCloseButton = true,
|
|
|
83
85
|
secondaryButtonProps,
|
|
84
86
|
secondaryButtonText,
|
|
85
87
|
]);
|
|
86
|
-
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,
|
|
88
|
+
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, hasDescription && !image ? (descriptionIsText ? (_jsx(BodyText, { accessible: true, inverted: isBrandBackground, children: description })) : (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, hasDescription ? (descriptionIsText ? (_jsx(BodyText, { textAlign: "center", accessible: true, inverted: isBrandBackground, children: description })) : (description)) : null] })] })) : null, scrollable ? (_jsxs(ScrollView, { style: {
|
|
87
89
|
flex: stickyFooter ? 1 : 0,
|
|
88
90
|
marginHorizontal: -4,
|
|
89
91
|
}, contentContainerStyle: { paddingHorizontal: 4 }, ...scrollViewProps, children: [children, !stickyFooter && hasFooter ? (_jsx(View, { style: [styles.inNavModalFooterContainer, footerStyle], children: footerContent })) : null] })) : (_jsxs(View, { style: {
|
package/docs/changelog.mdx
CHANGED
|
@@ -9,6 +9,37 @@ 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.34.5
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- [#1384](https://github.com/utilitywarehouse/hearth/pull/1384) [`be839e2`](https://github.com/utilitywarehouse/hearth/commit/be839e2ca46f079a0fe867f976044f4f31cca551) Thanks [@jordmccord](https://github.com/jordmccord)! - 🌟 [FEATURE]: `NavModal` and `Modal` `description` prop accepts JSX
|
|
17
|
+
|
|
18
|
+
`description` now accepts `ReactNode` in addition to a string, so you can pass
|
|
19
|
+
custom content such as links or styled text alongside the default text
|
|
20
|
+
styling.
|
|
21
|
+
|
|
22
|
+
**Components affected**:
|
|
23
|
+
|
|
24
|
+
- `NavModal`
|
|
25
|
+
- `Modal`
|
|
26
|
+
|
|
27
|
+
**Developer changes**:
|
|
28
|
+
|
|
29
|
+
No changes required for existing usage. To pass custom content, provide JSX
|
|
30
|
+
instead of a string:
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
<Modal
|
|
34
|
+
heading="Update available"
|
|
35
|
+
description={
|
|
36
|
+
<BodyText>
|
|
37
|
+
Read the <InlineLink onPress={() => {}}>release notes</InlineLink> before updating.
|
|
38
|
+
</BodyText>
|
|
39
|
+
}
|
|
40
|
+
/>
|
|
41
|
+
```
|
|
42
|
+
|
|
12
43
|
## 0.34.4
|
|
13
44
|
|
|
14
45
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@utilitywarehouse/hearth-react-native",
|
|
3
|
-
"version": "0.34.
|
|
3
|
+
"version": "0.34.5",
|
|
4
4
|
"description": "Utility Warehouse React Native UI library",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -60,9 +60,9 @@
|
|
|
60
60
|
"vite-plugin-svgr": "^4.5.0",
|
|
61
61
|
"vitest": "^4.1.7",
|
|
62
62
|
"@utilitywarehouse/hearth-storybook-utils": "0.0.0",
|
|
63
|
+
"@utilitywarehouse/hearth-react-icons": "^0.8.6",
|
|
64
|
+
"@utilitywarehouse/hearth-react-native-icons": "^0.8.5",
|
|
63
65
|
"@utilitywarehouse/hearth-fonts": "^0.1.1",
|
|
64
|
-
"@utilitywarehouse/hearth-react-icons": "^0.8.5",
|
|
65
|
-
"@utilitywarehouse/hearth-react-native-icons": "^0.8.4",
|
|
66
66
|
"@utilitywarehouse/hearth-svg-assets": "^0.6.4",
|
|
67
67
|
"@utilitywarehouse/hearth-tokens": "^0.4.1"
|
|
68
68
|
},
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { Canvas, Controls, Meta, Story } from '@storybook/addon-docs/blocks';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
BodyText,
|
|
4
|
+
BottomSheetModal,
|
|
5
|
+
Box,
|
|
6
|
+
Button,
|
|
7
|
+
Center,
|
|
8
|
+
Flex,
|
|
9
|
+
Heading,
|
|
10
|
+
InlineLink,
|
|
11
|
+
Modal,
|
|
12
|
+
} from '../../';
|
|
3
13
|
import StorybookLink from '../../../../../shared/storybook/StorybookLink';
|
|
4
14
|
import { BackToTopButton, UsageWrap, ViewFigmaButton } from '../../../docs/components';
|
|
5
15
|
import * as Stories from './Modal.stories';
|
|
@@ -31,6 +41,7 @@ If you need a modal layout inside a React Navigation modal screen, use <Storyboo
|
|
|
31
41
|
- [Modal with Image](#modal-with-image)
|
|
32
42
|
- [Fullscreen Modal](#fullscreen-modal)
|
|
33
43
|
- [Modal with Custom Content](#modal-with-custom-content)
|
|
44
|
+
- [JSX Description](#jsx-description)
|
|
34
45
|
- [Sticky Custom Footer](#sticky-custom-footer)
|
|
35
46
|
- [Loading State](#loading-state)
|
|
36
47
|
- [Without Close Button](#without-close-button)
|
|
@@ -93,30 +104,30 @@ const MyComponent = () => {
|
|
|
93
104
|
|
|
94
105
|
The Modal component extends the `BottomSheetModal` component and accepts all of its props, plus the following additional props:
|
|
95
106
|
|
|
96
|
-
| Property | Type | Description
|
|
97
|
-
| ----------------------------- | ------------------------------------------------------------------ |
|
|
98
|
-
| `heading` | `string` | The heading text displayed at the top of the modal
|
|
99
|
-
| `description` | `
|
|
100
|
-
| `showCloseButton` | `boolean` | Whether to show the close button in the top-right corner
|
|
101
|
-
| `primaryButtonText` | `string` | Text for the primary action button
|
|
102
|
-
| `secondaryButtonText` | `string` | Text for the secondary action button
|
|
103
|
-
| `onPressPrimaryButton` | `() => void` | Callback function called when the primary button is pressed
|
|
104
|
-
| `onPressSecondaryButton` | `() => void` | Callback function called when the secondary button is pressed
|
|
105
|
-
| `onPressCloseButton` | `() => void` | Callback function called when the close button is pressed
|
|
106
|
-
| `closeOnPrimaryButtonPress` | `boolean` | Whether to automatically close the modal when the primary button is pressed
|
|
107
|
-
| `closeOnSecondaryButtonPress` | `boolean` | Whether to automatically close the modal when the secondary button is pressed
|
|
108
|
-
| `onChange` | `(index: number, position: number, `<br />` type: number) => void` | Callback function called when the modal's position changes \*
|
|
109
|
-
| `loading` | `boolean` | Whether to show a loading state with spinner
|
|
110
|
-
| `loadingHeading` | `string` | The heading text to be displayed when loading is true. If not provided, the regular heading will be shown.
|
|
111
|
-
| `loadingDescription` | `string` | The description text to be displayed when loading is true. If not provided, the regular description will be shown.
|
|
112
|
-
| `image` | `ImageProps` | Image to display in the modal (shows as centered content with text below)
|
|
113
|
-
| `children` | `ReactNode` | Custom content to display in the modal body
|
|
114
|
-
| `primaryButtonProps` | `Omit<ButtonWithoutChildrenProps, 'children'>` | Additional props to pass to the primary button (colorScheme defaults to 'highlight', variant to 'solid')
|
|
115
|
-
| `secondaryButtonProps` | `Omit<ButtonWithoutChildrenProps, 'children'>` | Additional props to pass to the secondary button (colorScheme defaults to 'functional', variant to 'outline')
|
|
116
|
-
| `footer` | `ReactNode` | Custom footer content that replaces the built-in action buttons
|
|
117
|
-
| `footerStyle` | `StyleProp<ViewStyle>` | Styles applied to the footer container, useful for sticky footer shadows or custom spacing
|
|
118
|
-
| `closeButtonProps` | `Omit<UnstyledIconButtonProps, 'children'>` | Additional props to pass to the close button
|
|
119
|
-
| `fullscreen` | `boolean` | Whether the modal should take up the full screen height
|
|
107
|
+
| Property | Type | Description | Default |
|
|
108
|
+
| ----------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | -------------- |
|
|
109
|
+
| `heading` | `string` | The heading text displayed at the top of the modal | - |
|
|
110
|
+
| `description` | `ReactNode` | The description content displayed below the heading. Pass a string for the default styling, or JSX for custom content. | - |
|
|
111
|
+
| `showCloseButton` | `boolean` | Whether to show the close button in the top-right corner | `true` |
|
|
112
|
+
| `primaryButtonText` | `string` | Text for the primary action button | - |
|
|
113
|
+
| `secondaryButtonText` | `string` | Text for the secondary action button | - |
|
|
114
|
+
| `onPressPrimaryButton` | `() => void` | Callback function called when the primary button is pressed | - |
|
|
115
|
+
| `onPressSecondaryButton` | `() => void` | Callback function called when the secondary button is pressed | - |
|
|
116
|
+
| `onPressCloseButton` | `() => void` | Callback function called when the close button is pressed | - |
|
|
117
|
+
| `closeOnPrimaryButtonPress` | `boolean` | Whether to automatically close the modal when the primary button is pressed | `true` |
|
|
118
|
+
| `closeOnSecondaryButtonPress` | `boolean` | Whether to automatically close the modal when the secondary button is pressed | `true` |
|
|
119
|
+
| `onChange` | `(index: number, position: number, `<br />` type: number) => void` | Callback function called when the modal's position changes \* | - |
|
|
120
|
+
| `loading` | `boolean` | Whether to show a loading state with spinner | `false` |
|
|
121
|
+
| `loadingHeading` | `string` | The heading text to be displayed when loading is true. If not provided, the regular heading will be shown. | `'Loading...'` |
|
|
122
|
+
| `loadingDescription` | `string` | The description text to be displayed when loading is true. If not provided, the regular description will be shown. | - |
|
|
123
|
+
| `image` | `ImageProps` | Image to display in the modal (shows as centered content with text below) | - |
|
|
124
|
+
| `children` | `ReactNode` | Custom content to display in the modal body | - |
|
|
125
|
+
| `primaryButtonProps` | `Omit<ButtonWithoutChildrenProps, 'children'>` | Additional props to pass to the primary button (colorScheme defaults to 'highlight', variant to 'solid') | - |
|
|
126
|
+
| `secondaryButtonProps` | `Omit<ButtonWithoutChildrenProps, 'children'>` | Additional props to pass to the secondary button (colorScheme defaults to 'functional', variant to 'outline') | - |
|
|
127
|
+
| `footer` | `ReactNode` | Custom footer content that replaces the built-in action buttons | - |
|
|
128
|
+
| `footerStyle` | `StyleProp<ViewStyle>` | Styles applied to the footer container, useful for sticky footer shadows or custom spacing | - |
|
|
129
|
+
| `closeButtonProps` | `Omit<UnstyledIconButtonProps, 'children'>` | Additional props to pass to the close button | - |
|
|
130
|
+
| `fullscreen` | `boolean` | Whether the modal should take up the full screen height | `false` |
|
|
120
131
|
|
|
121
132
|
When `footer` is provided, the primary and secondary button props are not available. Build your footer actions directly inside the custom footer content instead.
|
|
122
133
|
|
|
@@ -381,6 +392,22 @@ const CustomContentModal = () => {
|
|
|
381
392
|
};
|
|
382
393
|
```
|
|
383
394
|
|
|
395
|
+
### JSX Description
|
|
396
|
+
|
|
397
|
+
`description` accepts a string for the default text styling, or JSX when you need custom content such as a link:
|
|
398
|
+
|
|
399
|
+
```tsx
|
|
400
|
+
<Modal
|
|
401
|
+
ref={modalRef}
|
|
402
|
+
heading="Update available"
|
|
403
|
+
description={
|
|
404
|
+
<BodyText>
|
|
405
|
+
Read the <InlineLink onPress={() => {}}>release notes</InlineLink> before updating.
|
|
406
|
+
</BodyText>
|
|
407
|
+
}
|
|
408
|
+
/>
|
|
409
|
+
```
|
|
410
|
+
|
|
384
411
|
### Sticky Custom Footer
|
|
385
412
|
|
|
386
413
|
Replace the built-in buttons with a custom sticky footer when you need custom layouts or button arrangements:
|
|
@@ -10,7 +10,7 @@ export interface ModalCommonBaseProps {
|
|
|
10
10
|
heading?: string;
|
|
11
11
|
loadingHeading?: string;
|
|
12
12
|
loadingDescription?: string;
|
|
13
|
-
description?:
|
|
13
|
+
description?: ReactNode;
|
|
14
14
|
stickyFooter?: boolean;
|
|
15
15
|
children?: ViewProps['children'];
|
|
16
16
|
onPressCloseButton?: () => void;
|
|
@@ -117,6 +117,8 @@ const Modal = ({
|
|
|
117
117
|
const hasSecondaryButton = !!(onPressSecondaryButton && secondaryButtonText);
|
|
118
118
|
const hasFooter = !!footer || hasPrimaryButton || hasSecondaryButton;
|
|
119
119
|
const shouldShowFooter = !loading && hasFooter;
|
|
120
|
+
const descriptionIsText = typeof description === 'string' || typeof description === 'number';
|
|
121
|
+
const hasDescription = description !== undefined && description !== null;
|
|
120
122
|
|
|
121
123
|
styles.useVariants({
|
|
122
124
|
loading,
|
|
@@ -197,7 +199,13 @@ const Modal = ({
|
|
|
197
199
|
{heading}
|
|
198
200
|
</Heading>
|
|
199
201
|
) : null}
|
|
200
|
-
{
|
|
202
|
+
{hasDescription && !image ? (
|
|
203
|
+
descriptionIsText ? (
|
|
204
|
+
<BodyText accessible>{description}</BodyText>
|
|
205
|
+
) : (
|
|
206
|
+
description
|
|
207
|
+
)
|
|
208
|
+
) : null}
|
|
201
209
|
</View>
|
|
202
210
|
{showCloseButton ? (
|
|
203
211
|
<UnstyledIconButton
|
|
@@ -217,10 +225,14 @@ const Modal = ({
|
|
|
217
225
|
{heading}
|
|
218
226
|
</Heading>
|
|
219
227
|
) : null}
|
|
220
|
-
{
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
228
|
+
{hasDescription ? (
|
|
229
|
+
descriptionIsText ? (
|
|
230
|
+
<BodyText textAlign="center" accessible>
|
|
231
|
+
{description}
|
|
232
|
+
</BodyText>
|
|
233
|
+
) : (
|
|
234
|
+
description
|
|
235
|
+
)
|
|
224
236
|
) : null}
|
|
225
237
|
</View>
|
|
226
238
|
</View>
|
|
@@ -96,6 +96,8 @@ const Modal = ({
|
|
|
96
96
|
const hasPrimaryButton = !!(onPressPrimaryButton && primaryButtonText);
|
|
97
97
|
const hasSecondaryButton = !!(onPressSecondaryButton && secondaryButtonText);
|
|
98
98
|
const hasFooter = !!footer || hasPrimaryButton || hasSecondaryButton;
|
|
99
|
+
const descriptionIsText = typeof description === 'string' || typeof description === 'number';
|
|
100
|
+
const hasDescription = description !== undefined && description !== null;
|
|
99
101
|
|
|
100
102
|
const footerContent = footer ?? (
|
|
101
103
|
<View style={styles.footer}>
|
|
@@ -150,7 +152,13 @@ const Modal = ({
|
|
|
150
152
|
{heading}
|
|
151
153
|
</Heading>
|
|
152
154
|
) : null}
|
|
153
|
-
{
|
|
155
|
+
{hasDescription && !image ? (
|
|
156
|
+
descriptionIsText ? (
|
|
157
|
+
<BodyText accessible>{description}</BodyText>
|
|
158
|
+
) : (
|
|
159
|
+
description
|
|
160
|
+
)
|
|
161
|
+
) : null}
|
|
154
162
|
</View>
|
|
155
163
|
{showCloseButton ? (
|
|
156
164
|
<UnstyledIconButton
|
|
@@ -170,10 +178,14 @@ const Modal = ({
|
|
|
170
178
|
{heading}
|
|
171
179
|
</Heading>
|
|
172
180
|
) : null}
|
|
173
|
-
{
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
181
|
+
{hasDescription ? (
|
|
182
|
+
descriptionIsText ? (
|
|
183
|
+
<BodyText textAlign="center" accessible>
|
|
184
|
+
{description}
|
|
185
|
+
</BodyText>
|
|
186
|
+
) : (
|
|
187
|
+
description
|
|
188
|
+
)
|
|
177
189
|
) : null}
|
|
178
190
|
</View>
|
|
179
191
|
</View>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks';
|
|
2
|
-
import { BodyText, Button, Flex } from '../../';
|
|
2
|
+
import { BodyText, Button, Flex, InlineLink } from '../../';
|
|
3
3
|
import StorybookLink from '../../../../../shared/storybook/StorybookLink';
|
|
4
4
|
import modalAndroidVideo from '../../../docs/assets/modal-android.mp4';
|
|
5
5
|
import modaliOSVideo from '../../../docs/assets/modal-ios.mp4';
|
|
@@ -139,7 +139,7 @@ const styles = StyleSheet.create({
|
|
|
139
139
|
| Property | Type | Description | Default |
|
|
140
140
|
| ------------------------ | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------- |
|
|
141
141
|
| `heading` | `string` | Heading text shown at the top of the modal when no `image` is provided. | - |
|
|
142
|
-
| `description` | `
|
|
142
|
+
| `description` | `ReactNode` | Supporting text shown below the heading when no `image` is provided. Pass a string for the default styling, or JSX for custom content. | - |
|
|
143
143
|
| `showCloseButton` | `boolean` | Whether to render the close button in the top-right corner. | `true` |
|
|
144
144
|
| `primaryButtonText` | `string` | Label for the primary action button. | - |
|
|
145
145
|
| `secondaryButtonText` | `string` | Label for the secondary action button. | - |
|
|
@@ -206,3 +206,18 @@ When `footer` is provided, the primary and secondary button props are not availa
|
|
|
206
206
|
<BodyText>This sticky footer stays pinned while the content scrolls.</BodyText>
|
|
207
207
|
</NavModal>
|
|
208
208
|
```
|
|
209
|
+
|
|
210
|
+
### JSX Description
|
|
211
|
+
|
|
212
|
+
`description` accepts a string for the default text styling, or JSX when you need custom content such as a link.
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
<NavModal
|
|
216
|
+
heading="Update available"
|
|
217
|
+
description={
|
|
218
|
+
<BodyText>
|
|
219
|
+
Read the <InlineLink onPress={() => {}}>release notes</InlineLink> before updating.
|
|
220
|
+
</BodyText>
|
|
221
|
+
}
|
|
222
|
+
/>
|
|
223
|
+
```
|
|
@@ -4,6 +4,7 @@ import { BodyText } from '../BodyText';
|
|
|
4
4
|
import { Box } from '../Box';
|
|
5
5
|
import { Button } from '../Button';
|
|
6
6
|
import { Flex } from '../Flex';
|
|
7
|
+
import { LI, UL } from '../HTMLElements';
|
|
7
8
|
import NavModal from './NavModal';
|
|
8
9
|
|
|
9
10
|
const meta = {
|
|
@@ -158,3 +159,42 @@ export const StickyCustomFooter: Story = {
|
|
|
158
159
|
</View>
|
|
159
160
|
),
|
|
160
161
|
};
|
|
162
|
+
|
|
163
|
+
export const WithJSXDescription: Story = {
|
|
164
|
+
render: () => (
|
|
165
|
+
<View style={Platform.OS === 'web' ? { width: 400, height: 720 } : { flex: 1 }}>
|
|
166
|
+
<NavModal
|
|
167
|
+
heading="Confirm changes"
|
|
168
|
+
description={
|
|
169
|
+
<>
|
|
170
|
+
<BodyText>
|
|
171
|
+
This example uses a JSX element for the description prop. You can use any React
|
|
172
|
+
component here, including links, lists, and other text components.
|
|
173
|
+
</BodyText>
|
|
174
|
+
<UL>
|
|
175
|
+
<LI>
|
|
176
|
+
<BodyText>Item 1</BodyText>
|
|
177
|
+
</LI>
|
|
178
|
+
</UL>
|
|
179
|
+
<UL>
|
|
180
|
+
<LI>
|
|
181
|
+
<BodyText>Item 2</BodyText>
|
|
182
|
+
</LI>
|
|
183
|
+
</UL>
|
|
184
|
+
<UL>
|
|
185
|
+
<LI>
|
|
186
|
+
<BodyText>Item 3</BodyText>
|
|
187
|
+
</LI>
|
|
188
|
+
</UL>
|
|
189
|
+
</>
|
|
190
|
+
}
|
|
191
|
+
>
|
|
192
|
+
<Box gap="200">
|
|
193
|
+
<BodyText>
|
|
194
|
+
Use the description prop to provide additional context or instructions.
|
|
195
|
+
</BodyText>
|
|
196
|
+
</Box>
|
|
197
|
+
</NavModal>
|
|
198
|
+
</View>
|
|
199
|
+
),
|
|
200
|
+
};
|
|
@@ -115,6 +115,8 @@ const NavModal = ({
|
|
|
115
115
|
const hasPrimaryButton = !!(onPressPrimaryButton && primaryButtonText);
|
|
116
116
|
const hasSecondaryButton = !!(onPressSecondaryButton && secondaryButtonText);
|
|
117
117
|
const hasFooter = !!footer || hasPrimaryButton || hasSecondaryButton;
|
|
118
|
+
const descriptionIsText = typeof description === 'string' || typeof description === 'number';
|
|
119
|
+
const hasDescription = description !== undefined && description !== null;
|
|
118
120
|
|
|
119
121
|
styles.useVariants({
|
|
120
122
|
loading,
|
|
@@ -201,10 +203,14 @@ const NavModal = ({
|
|
|
201
203
|
{heading}
|
|
202
204
|
</Heading>
|
|
203
205
|
) : null}
|
|
204
|
-
{
|
|
205
|
-
|
|
206
|
-
{
|
|
207
|
-
|
|
206
|
+
{hasDescription && !image ? (
|
|
207
|
+
descriptionIsText ? (
|
|
208
|
+
<BodyText accessible inverted={isBrandBackground}>
|
|
209
|
+
{description}
|
|
210
|
+
</BodyText>
|
|
211
|
+
) : (
|
|
212
|
+
description
|
|
213
|
+
)
|
|
208
214
|
) : null}
|
|
209
215
|
</View>
|
|
210
216
|
{showCloseButton ? (
|
|
@@ -226,10 +232,14 @@ const NavModal = ({
|
|
|
226
232
|
{heading}
|
|
227
233
|
</Heading>
|
|
228
234
|
) : null}
|
|
229
|
-
{
|
|
230
|
-
|
|
231
|
-
{
|
|
232
|
-
|
|
235
|
+
{hasDescription ? (
|
|
236
|
+
descriptionIsText ? (
|
|
237
|
+
<BodyText textAlign="center" accessible inverted={isBrandBackground}>
|
|
238
|
+
{description}
|
|
239
|
+
</BodyText>
|
|
240
|
+
) : (
|
|
241
|
+
description
|
|
242
|
+
)
|
|
233
243
|
) : null}
|
|
234
244
|
</View>
|
|
235
245
|
</View>
|