@butternutbox/pawprint-native 1.3.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@butternutbox/pawprint-native",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "type": "module",
5
5
  "description": "ButternutBox Pawprint Design System - React Native Components",
6
6
  "main": "./dist/index.cjs",
@@ -41,8 +41,8 @@ export default {
41
41
  },
42
42
  colour: {
43
43
  control: { type: "select" },
44
- options: ["primary", "secondary"],
45
- description: "Colour scheme"
44
+ options: ["primary", "secondary", "tertiary"],
45
+ description: "Colour scheme (filled only)"
46
46
  },
47
47
  loading: {
48
48
  control: { type: "boolean" },
@@ -86,7 +86,7 @@ export const AllVariants = () => (
86
86
  {variant}
87
87
  </Typography>
88
88
  <View style={styles.row}>
89
- {(["primary", "secondary"] as const).map((colour) => (
89
+ {(["primary", "secondary", "tertiary"] as const).map((colour) => (
90
90
  <Button key={colour} variant={variant} colour={colour}>
91
91
  {colour}
92
92
  </Button>
@@ -120,7 +120,7 @@ export const AllStates = () => (
120
120
  <View style={styles.column}>
121
121
  {(["filled", "outlined", "text"] as const).map((variant) => (
122
122
  <View key={variant} style={styles.section}>
123
- {(["primary", "secondary"] as const).map((colour) => (
123
+ {(["primary", "secondary", "tertiary"] as const).map((colour) => (
124
124
  <View key={colour} style={styles.section}>
125
125
  <Typography size="sm" weight="semiBold" color="tertiary">
126
126
  {variant} / {colour}
@@ -2,7 +2,7 @@ import React from "react"
2
2
  import { screen } from "@testing-library/react"
3
3
  import userEvent from "@testing-library/user-event"
4
4
  import { describe, it, expect, vi } from "vitest"
5
- import { renderWithTheme } from "../../../test-utils"
5
+ import { renderWithTheme, theme } from "../../../test-utils"
6
6
  import { Button } from "./Button"
7
7
 
8
8
  describe("Button", () => {
@@ -36,13 +36,20 @@ describe("Button", () => {
36
36
  })
37
37
 
38
38
  describe("when rendering colours", () => {
39
- it.each(["primary", "secondary"] as const)(
39
+ it.each(["primary", "secondary", "tertiary"] as const)(
40
40
  "renders %s colour",
41
41
  (colour) => {
42
42
  renderWithTheme(<Button colour={colour}>Colour</Button>)
43
43
  expect(screen.getByText("Colour")).toBeInTheDocument()
44
44
  }
45
45
  )
46
+
47
+ it("applies the tertiary text colour", () => {
48
+ renderWithTheme(<Button colour="tertiary">Tertiary</Button>)
49
+ expect(screen.getByText("Tertiary")).toHaveStyle({
50
+ color: theme.tokens.semantics.colour.text.action.default
51
+ })
52
+ })
46
53
  })
47
54
 
48
55
  describe("when component is interactive", () => {
@@ -1,14 +1,14 @@
1
1
  import React from "react"
2
2
  import { Pressable, View, PressableProps } from "react-native"
3
3
  import styled from "@emotion/native"
4
- import { useTheme } from "@emotion/react"
4
+ import { useTheme, type Theme } from "@emotion/react"
5
5
  import { Typography } from "../Typography"
6
6
  import { Spinner } from "../Spinner"
7
7
  import { resolveFont } from "@butternutbox/pawprint-fonts"
8
8
 
9
9
  type ButtonVariant = "filled" | "outlined" | "text"
10
10
  type ButtonSize = "sm" | "md" | "lg"
11
- type ButtonColour = "primary" | "secondary"
11
+ type ButtonColour = "primary" | "secondary" | "tertiary"
12
12
 
13
13
  type ButtonOwnProps = {
14
14
  variant?: ButtonVariant
@@ -26,6 +26,35 @@ type ButtonProps = ButtonOwnProps &
26
26
 
27
27
  const parseTokenValue = (value: string): number => parseFloat(value)
28
28
 
29
+ type FilledColourTokens = {
30
+ background: {
31
+ default: string
32
+ selected: string
33
+ disabled: string
34
+ }
35
+ text: { default: string; selected?: string }
36
+ }
37
+
38
+ const getFilledColourTokens = (
39
+ theme: Theme,
40
+ colour: ButtonColour
41
+ ): FilledColourTokens => {
42
+ const buttons = theme.tokens.components.buttons
43
+
44
+ if (colour === "tertiary") {
45
+ const { text } = theme.tokens.semantics.colour
46
+ return {
47
+ background: buttons.iconButton.filledButton.colour.background.tertiary,
48
+ text: { default: text.action.default, selected: text.action.inverse }
49
+ }
50
+ }
51
+
52
+ return {
53
+ background: buttons.filledButton.colour.background[colour],
54
+ text: buttons.filledButton.colour.text[colour]
55
+ }
56
+ }
57
+
29
58
  const sizeToTypographyKey: Record<ButtonSize, string> = {
30
59
  lg: "large",
31
60
  md: "medium",
@@ -104,7 +133,7 @@ const StyledSpinnerWrapper = styled(View)({
104
133
  *
105
134
  * @param variant - *(optional)* Visual variant: filled (default), outlined, or text.
106
135
  * @param size - *(optional)* Size variant: sm, md (default), lg
107
- * @param colour - *(optional)* Colour variant: primary (default), secondary
136
+ * @param colour - *(optional)* Colour variant: primary (default), secondary, tertiary. Only applies to filled variant.
108
137
  * @param loading - *(optional)* Shows spinner and disables interaction
109
138
  * @param fullWidth - *(optional)* Stretches button to fill its container
110
139
  * @param startIcon - *(optional)* Leading icon element
@@ -150,16 +179,19 @@ const Button = React.forwardRef<View, ButtonProps>(
150
179
  spinnerColor: string
151
180
  } => {
152
181
  if (variant === "filled") {
153
- const bgTokens = buttons.filledButton.colour.background[colour]
154
- const textToken = buttons.filledButton.colour.text[colour].default
182
+ const { background: bgTokens, text: textTokens } =
183
+ getFilledColourTokens(theme, colour)
155
184
  return {
156
185
  backgroundColor: isDisabled
157
186
  ? bgTokens.disabled
158
187
  : isPressed
159
188
  ? bgTokens.selected
160
189
  : bgTokens.default,
161
- textColor: textToken,
162
- spinnerColor: textToken
190
+ textColor:
191
+ isPressed && textTokens.selected
192
+ ? textTokens.selected
193
+ : textTokens.default,
194
+ spinnerColor: textTokens.default
163
195
  }
164
196
  }
165
197
 
@@ -0,0 +1,140 @@
1
+ import React from "react"
2
+ import { View, StyleSheet } from "react-native"
3
+ import { Add, Search, User } from "@butternutbox/pawprint-icons/core"
4
+ import {
5
+ NavigationHeader,
6
+ type NavigationHeaderProps,
7
+ type NavigationHeaderSurface
8
+ } from "./NavigationHeader"
9
+ import { Typography } from "../../atoms/Typography"
10
+
11
+ const actionElements: Record<string, React.ReactNode> = {
12
+ None: undefined,
13
+ Button: <NavigationHeader.Button>Pouch quantity</NavigationHeader.Button>,
14
+ "Icon buttons + Button": (
15
+ <>
16
+ <NavigationHeader.IconButton icon={Search} aria-label="Search" />
17
+ <NavigationHeader.IconButton icon={Add} aria-label="Add" />
18
+ <NavigationHeader.Button>Button</NavigationHeader.Button>
19
+ </>
20
+ ),
21
+ "Primary button": (
22
+ <NavigationHeader.Button variant="filled" colour="primary">
23
+ Button
24
+ </NavigationHeader.Button>
25
+ )
26
+ }
27
+
28
+ export default {
29
+ title: "Molecules/NavigationHeader",
30
+ component: NavigationHeader,
31
+ argTypes: {
32
+ title: {
33
+ control: { type: "text" },
34
+ description: "Page title, truncated to one line."
35
+ },
36
+ variant: {
37
+ control: { type: "select" },
38
+ options: ["primary", "secondary"],
39
+ description:
40
+ "Header type. `primary` for landing pages; `secondary` adds a back button for child pages."
41
+ },
42
+ surface: {
43
+ control: { type: "select" },
44
+ options: ["brand", "subtle", "plain"],
45
+ description: "Background colour."
46
+ },
47
+ backLabel: {
48
+ control: { type: "text" },
49
+ description: "Accessible label for the back button."
50
+ },
51
+ showRadiusPips: {
52
+ control: { type: "boolean" },
53
+ description:
54
+ "Shows the rounded corners below the header. Defaults to `true` on the `brand` surface only."
55
+ },
56
+ safeAreaTop: {
57
+ control: { type: "boolean" },
58
+ description: "Pads the header by the top safe area inset."
59
+ },
60
+ actions: {
61
+ control: { type: "select" },
62
+ options: Object.keys(actionElements),
63
+ mapping: actionElements,
64
+ description: "Right-hand action group."
65
+ }
66
+ }
67
+ }
68
+
69
+ export const Playground = (args: NavigationHeaderProps) => (
70
+ <NavigationHeader {...args} />
71
+ )
72
+ Playground.args = {
73
+ title: "Title",
74
+ variant: "secondary",
75
+ surface: "brand",
76
+ backLabel: "Back",
77
+ safeAreaTop: false,
78
+ actions: actionElements["Icon buttons + Button"]
79
+ }
80
+
81
+ export const AllVariants = () => (
82
+ <View style={styles.column}>
83
+ {(["primary", "secondary"] as const).map((variant) => (
84
+ <NavigationHeader
85
+ key={variant}
86
+ variant={variant}
87
+ title={variant === "primary" ? "Hello, Katie" : "My dog"}
88
+ safeAreaTop={false}
89
+ actions={
90
+ <>
91
+ <NavigationHeader.IconButton icon={User} aria-label="Account" />
92
+ <NavigationHeader.Button>Button</NavigationHeader.Button>
93
+ </>
94
+ }
95
+ />
96
+ ))}
97
+ </View>
98
+ )
99
+
100
+ export const AllSurfaces = () => (
101
+ <View style={styles.column}>
102
+ {(["brand", "subtle", "plain"] as NavigationHeaderSurface[]).map(
103
+ (surface) => (
104
+ <View key={surface} style={styles.section}>
105
+ <Typography size="sm" weight="semiBold" color="tertiary">
106
+ {surface}
107
+ </Typography>
108
+ <NavigationHeader
109
+ variant="secondary"
110
+ surface={surface}
111
+ title="Recipes"
112
+ safeAreaTop={false}
113
+ actions={
114
+ <>
115
+ <NavigationHeader.IconButton icon={Add} aria-label="Add" />
116
+ <NavigationHeader.Button>
117
+ Pouch quantity
118
+ </NavigationHeader.Button>
119
+ </>
120
+ }
121
+ />
122
+ </View>
123
+ )
124
+ )}
125
+ </View>
126
+ )
127
+
128
+ export const LongTitle = () => (
129
+ <NavigationHeader
130
+ variant="secondary"
131
+ title="A very long page title that should truncate before the actions"
132
+ safeAreaTop={false}
133
+ actions={<NavigationHeader.Button>Pouch quantity</NavigationHeader.Button>}
134
+ />
135
+ )
136
+
137
+ const styles = StyleSheet.create({
138
+ column: { flexDirection: "column", gap: 24 },
139
+ section: { gap: 8 }
140
+ })
@@ -0,0 +1,110 @@
1
+ import React from "react"
2
+ import { screen } from "@testing-library/react"
3
+ import userEvent from "@testing-library/user-event"
4
+ import { describe, it, expect, vi } from "vitest"
5
+ import { renderWithTheme } from "../../../test-utils"
6
+ import { NavigationHeader } from "./NavigationHeader"
7
+
8
+ const MockIcon = () => <span data-testid="mock-icon" />
9
+
10
+ describe("NavigationHeader", () => {
11
+ describe("when component is rendering", () => {
12
+ it("renders the title", () => {
13
+ renderWithTheme(<NavigationHeader title="Recipes" />)
14
+ expect(screen.getByText("Recipes")).toBeInTheDocument()
15
+ })
16
+
17
+ it.each(["primary", "secondary"] as const)(
18
+ "renders the %s variant",
19
+ (variant) => {
20
+ renderWithTheme(<NavigationHeader title="Recipes" variant={variant} />)
21
+ expect(screen.getByText("Recipes")).toBeInTheDocument()
22
+ }
23
+ )
24
+
25
+ it.each(["brand", "subtle", "plain"] as const)(
26
+ "renders the %s surface",
27
+ (surface) => {
28
+ renderWithTheme(<NavigationHeader title="Recipes" surface={surface} />)
29
+ expect(screen.getByText("Recipes")).toBeInTheDocument()
30
+ }
31
+ )
32
+ })
33
+
34
+ describe("when rendering the back button", () => {
35
+ it("does not render it for the primary variant", () => {
36
+ renderWithTheme(<NavigationHeader title="Home" onBack={vi.fn()} />)
37
+ expect(screen.queryByLabelText("Back")).not.toBeInTheDocument()
38
+ })
39
+
40
+ it("calls onBack when pressed", async () => {
41
+ const user = userEvent.setup()
42
+ const onBack = vi.fn()
43
+ renderWithTheme(
44
+ <NavigationHeader title="Recipes" variant="secondary" onBack={onBack} />
45
+ )
46
+
47
+ await user.click(screen.getByLabelText("Back"))
48
+ expect(onBack).toHaveBeenCalledTimes(1)
49
+ })
50
+
51
+ it("uses a custom back label", () => {
52
+ renderWithTheme(
53
+ <NavigationHeader
54
+ title="Recipes"
55
+ variant="secondary"
56
+ backLabel="Retour"
57
+ />
58
+ )
59
+ expect(screen.getByLabelText("Retour")).toBeInTheDocument()
60
+ })
61
+ })
62
+
63
+ describe("when rendering actions", () => {
64
+ it("renders text and icon actions", async () => {
65
+ const user = userEvent.setup()
66
+ const onPress = vi.fn()
67
+ renderWithTheme(
68
+ <NavigationHeader
69
+ title="Recipes"
70
+ actions={
71
+ <>
72
+ <NavigationHeader.IconButton icon={MockIcon} aria-label="Add" />
73
+ <NavigationHeader.Button onPress={onPress}>
74
+ Pouch quantity
75
+ </NavigationHeader.Button>
76
+ </>
77
+ }
78
+ />
79
+ )
80
+ expect(screen.getByLabelText("Add")).toBeInTheDocument()
81
+
82
+ await user.click(screen.getByText("Pouch quantity"))
83
+ expect(onPress).toHaveBeenCalledTimes(1)
84
+ })
85
+ })
86
+
87
+ describe("when rendering radius pips", () => {
88
+ const countPips = (container: HTMLElement) =>
89
+ container.querySelectorAll('[data-mock="Svg"]').length
90
+
91
+ it("renders them on the brand surface by default", () => {
92
+ const { container } = renderWithTheme(
93
+ <NavigationHeader title="Recipes" />
94
+ )
95
+ expect(countPips(container)).toBe(2)
96
+ })
97
+
98
+ it("hides them on other surfaces unless enabled", () => {
99
+ const { container, rerender } = renderWithTheme(
100
+ <NavigationHeader title="Recipes" surface="subtle" />
101
+ )
102
+ expect(countPips(container)).toBe(0)
103
+
104
+ rerender(
105
+ <NavigationHeader title="Recipes" surface="subtle" showRadiusPips />
106
+ )
107
+ expect(countPips(container)).toBe(2)
108
+ })
109
+ })
110
+ })
@@ -0,0 +1,273 @@
1
+ import React from "react"
2
+ import { View, ViewProps } from "react-native"
3
+ import Svg, { Path } from "react-native-svg"
4
+ import { useSafeAreaInsets } from "react-native-safe-area-context"
5
+ import styled from "@emotion/native"
6
+ import { useTheme, type Theme } from "@emotion/react"
7
+ import { ArrowBack } from "@butternutbox/pawprint-icons/core"
8
+ import { IconButton } from "../../atoms/IconButton"
9
+ import { Typography } from "../../atoms/Typography"
10
+ import {
11
+ NavigationHeaderButton,
12
+ NavigationHeaderIconButton
13
+ } from "./NavigationHeaderButton"
14
+ import {
15
+ NavigationHeaderContext,
16
+ getIconButtonColour,
17
+ type NavigationHeaderContextValue,
18
+ type NavigationHeaderSurface
19
+ } from "./NavigationHeaderContext"
20
+
21
+ type NavigationHeaderVariant = "primary" | "secondary"
22
+
23
+ type NavigationHeaderOwnProps = {
24
+ title: string
25
+ variant?: NavigationHeaderVariant
26
+ surface?: NavigationHeaderSurface
27
+ onBack?: () => void
28
+ backLabel?: string
29
+ actions?: React.ReactNode
30
+ showRadiusPips?: boolean
31
+ safeAreaTop?: boolean
32
+ }
33
+
34
+ export type NavigationHeaderProps = NavigationHeaderOwnProps &
35
+ Omit<ViewProps, keyof NavigationHeaderOwnProps | "children">
36
+
37
+ const parseTokenValue = (value: string): number => parseFloat(value)
38
+
39
+ const getSurfaceColour = (theme: Theme, surface: NavigationHeaderSurface) => {
40
+ const { navigationHeader } = theme.tokens.components
41
+ const { background } = theme.tokens.semantics.colour
42
+
43
+ return {
44
+ brand: navigationHeader.colour.surface.default,
45
+ subtle: background.surface.secondary,
46
+ plain: background.surface.default
47
+ }[surface]
48
+ }
49
+
50
+ const StyledNavContainer = styled(View)<{
51
+ containerPaddingTop: number
52
+ containerPaddingV: number
53
+ containerPaddingH: number
54
+ containerGap: number
55
+ containerBgColor: string
56
+ }>(
57
+ ({
58
+ containerPaddingTop,
59
+ containerPaddingV,
60
+ containerPaddingH,
61
+ containerGap,
62
+ containerBgColor
63
+ }) => ({
64
+ flexDirection: "row",
65
+ alignItems: "center",
66
+ paddingTop: containerPaddingTop + containerPaddingV,
67
+ paddingBottom: containerPaddingV,
68
+ paddingHorizontal: containerPaddingH,
69
+ gap: containerGap,
70
+ backgroundColor: containerBgColor
71
+ })
72
+ )
73
+
74
+ const StyledTitleGroup = styled(View)<{
75
+ titleGroupGap: number
76
+ titleGroupMinHeight: number
77
+ }>(({ titleGroupGap, titleGroupMinHeight }) => ({
78
+ flex: 1,
79
+ flexDirection: "row",
80
+ alignItems: "center",
81
+ minWidth: 0,
82
+ minHeight: titleGroupMinHeight,
83
+ gap: titleGroupGap
84
+ }))
85
+
86
+ const StyledTitle = styled(View)({
87
+ flex: 1,
88
+ minWidth: 0
89
+ })
90
+
91
+ const StyledActionGroup = styled(View)<{ actionGroupGap: number }>(
92
+ ({ actionGroupGap }) => ({
93
+ flexDirection: "row",
94
+ alignItems: "center",
95
+ flexShrink: 0,
96
+ gap: actionGroupGap
97
+ })
98
+ )
99
+
100
+ const StyledRadiusPips = styled(View)({
101
+ flexDirection: "row",
102
+ justifyContent: "space-between"
103
+ })
104
+
105
+ const RadiusPips = ({ radius, colour }: { radius: number; colour: string }) => (
106
+ <StyledRadiusPips
107
+ accessible={false}
108
+ importantForAccessibility="no-hide-descendants"
109
+ >
110
+ <Svg width={radius} height={radius}>
111
+ <Path
112
+ d={`M0 0H${radius}A${radius} ${radius} 0 0 0 0 ${radius}Z`}
113
+ fill={colour}
114
+ />
115
+ </Svg>
116
+ <Svg width={radius} height={radius}>
117
+ <Path
118
+ d={`M${radius} 0H0A${radius} ${radius} 0 0 1 ${radius} ${radius}Z`}
119
+ fill={colour}
120
+ />
121
+ </Svg>
122
+ </StyledRadiusPips>
123
+ )
124
+
125
+ /**
126
+ * Navigation header for the native app. Gives users context of where they
127
+ * are, a way back, and the most important page-level actions.
128
+ *
129
+ * - `primary` — landing pages: large title, no back button.
130
+ * - `secondary` — child pages: back button and a smaller title.
131
+ *
132
+ * Actions are passed via `actions`. Use `NavigationHeader.Button` and
133
+ * `NavigationHeader.IconButton` for the header defaults, which follow the
134
+ * surface; their `variant` and `colour` can be overridden. Keep to at most
135
+ * three.
136
+ *
137
+ * Note: Unlike the web version, there is no desktop layout, the header extends
138
+ * behind the status bar by applying the top safe area inset (`safeAreaTop`),
139
+ * and `onBack` is a press handler.
140
+ *
141
+ * @param {string} title - Page title, truncated to one line.
142
+ * @param {"primary" | "secondary"} [variant="primary"] - Header type.
143
+ * @param {"brand" | "subtle" | "plain"} [surface="brand"] - Background colour.
144
+ * `brand` is the yellow navigation surface, `subtle` the cream page surface
145
+ * and `plain` the default page surface.
146
+ * @param {() => void} [onBack] - Back button press handler. `secondary` only.
147
+ * @param {string} [backLabel="Back"] - Accessible label for the back button.
148
+ * The back button is `tertiary` on the `brand` surface and `secondary`
149
+ * otherwise.
150
+ * @param {React.ReactNode} [actions] - Right-hand action group.
151
+ * @param {boolean} [showRadiusPips] - Shows the rounded corners below the
152
+ * header. Defaults to `true` on the `brand` surface only.
153
+ * @param {boolean} [safeAreaTop=true] - Pads the header by the top safe area
154
+ * inset.
155
+ *
156
+ * @example
157
+ * ```tsx
158
+ * import { NavigationHeader } from "@butternutbox/pawprint-native"
159
+ *
160
+ * <NavigationHeader
161
+ * variant="secondary"
162
+ * title="Recipes"
163
+ * onBack={navigation.goBack}
164
+ * actions={
165
+ * <NavigationHeader.Button onPress={editQuantity}>
166
+ * Pouch quantity
167
+ * </NavigationHeader.Button>
168
+ * }
169
+ * />
170
+ * ```
171
+ */
172
+ const NavigationHeaderRoot = React.forwardRef<View, NavigationHeaderProps>(
173
+ (
174
+ {
175
+ title,
176
+ variant = "primary",
177
+ surface = "brand",
178
+ onBack,
179
+ backLabel = "Back",
180
+ actions,
181
+ showRadiusPips = surface === "brand",
182
+ safeAreaTop = true,
183
+ ...rest
184
+ },
185
+ ref
186
+ ) => {
187
+ const theme = useTheme()
188
+ const { top } = useSafeAreaInsets()
189
+ const { navigationHeader, buttons } = theme.tokens.components
190
+ const { spacing } = navigationHeader
191
+ const surfaceColour = getSurfaceColour(theme, surface)
192
+ const contextValue = React.useMemo<NavigationHeaderContextValue>(
193
+ () => ({ surface }),
194
+ [surface]
195
+ )
196
+
197
+ return (
198
+ <NavigationHeaderContext.Provider value={contextValue}>
199
+ <View ref={ref} {...rest}>
200
+ <StyledNavContainer
201
+ containerPaddingTop={safeAreaTop ? top : 0}
202
+ containerPaddingV={parseTokenValue(
203
+ spacing.navContainer.verticalPadding
204
+ )}
205
+ containerPaddingH={parseTokenValue(
206
+ spacing.navContainer.horizontalPadding
207
+ )}
208
+ containerGap={parseTokenValue(spacing.navContainer.gap)}
209
+ containerBgColor={surfaceColour}
210
+ >
211
+ <StyledTitleGroup
212
+ titleGroupGap={parseTokenValue(spacing.titleGroup.gap)}
213
+ titleGroupMinHeight={parseTokenValue(buttons.size.md.minHeight)}
214
+ >
215
+ {variant === "secondary" && (
216
+ <IconButton
217
+ icon={ArrowBack}
218
+ variant="filled"
219
+ colour={getIconButtonColour(contextValue)}
220
+ size="md"
221
+ aria-label={backLabel}
222
+ accessibilityRole="button"
223
+ onPress={onBack}
224
+ />
225
+ )}
226
+ <StyledTitle>
227
+ <Typography
228
+ token={navigationHeader.typography[variant]}
229
+ color={navigationHeader.colour.text.default}
230
+ accessibilityRole="header"
231
+ noWrap
232
+ >
233
+ {title}
234
+ </Typography>
235
+ </StyledTitle>
236
+ </StyledTitleGroup>
237
+ {actions && (
238
+ <StyledActionGroup
239
+ actionGroupGap={parseTokenValue(spacing.actionGroup.gap)}
240
+ >
241
+ {actions}
242
+ </StyledActionGroup>
243
+ )}
244
+ </StyledNavContainer>
245
+ {showRadiusPips && (
246
+ <RadiusPips
247
+ radius={parseTokenValue(
248
+ theme.tokens.semantics.dimensions.borderRadius.lg
249
+ )}
250
+ colour={surfaceColour}
251
+ />
252
+ )}
253
+ </View>
254
+ </NavigationHeaderContext.Provider>
255
+ )
256
+ }
257
+ )
258
+
259
+ NavigationHeaderRoot.displayName = "NavigationHeader"
260
+
261
+ type NavigationHeaderComponent = React.ForwardRefExoticComponent<
262
+ NavigationHeaderProps & React.RefAttributes<View>
263
+ > & {
264
+ Button: typeof NavigationHeaderButton
265
+ IconButton: typeof NavigationHeaderIconButton
266
+ }
267
+
268
+ export const NavigationHeader = Object.assign(NavigationHeaderRoot, {
269
+ Button: NavigationHeaderButton,
270
+ IconButton: NavigationHeaderIconButton
271
+ }) as NavigationHeaderComponent
272
+
273
+ export type { NavigationHeaderVariant, NavigationHeaderSurface }