@butternutbox/pawprint-native 0.10.6 → 0.10.8

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": "0.10.6",
3
+ "version": "0.10.8",
4
4
  "type": "module",
5
5
  "description": "ButternutBox Pawprint Design System - React Native Components",
6
6
  "main": "./dist/index.cjs",
@@ -10,6 +10,7 @@ import * as poses from "@butternutbox/pawprint-illustrations/poses"
10
10
  import * as usps from "@butternutbox/pawprint-illustrations/usps"
11
11
  import * as breeds from "@butternutbox/pawprint-illustrations/breeds"
12
12
  import * as breedSizes from "@butternutbox/pawprint-illustrations/breed-sizes"
13
+ import * as traits from "@butternutbox/pawprint-illustrations/traits"
13
14
 
14
15
  function isComponent(value: unknown): value is PawprintIllustration {
15
16
  if (typeof value === "function") return true
@@ -21,7 +22,8 @@ const categorised: Record<string, Record<string, unknown>> = {
21
22
  poses,
22
23
  usps,
23
24
  breeds,
24
- "breed-sizes": breedSizes
25
+ "breed-sizes": breedSizes,
26
+ traits
25
27
  }
26
28
 
27
29
  const illustrationMap: Record<string, PawprintIllustration> = {}
@@ -26,6 +26,7 @@ type InputFieldOwnProps = {
26
26
  hideStateIcons?: boolean
27
27
  containerWidth?: number
28
28
  containerHeight?: number
29
+ hasInlineHelp?: boolean
29
30
  }
30
31
 
31
32
  export type InputFieldProps = InputFieldOwnProps &
@@ -60,7 +61,10 @@ const StyledInputWrapper = styled(Animated.View)<{
60
61
  }
61
62
  })
62
63
 
63
- const StyledInput = styled(TextInput)(({ theme }) => {
64
+ const StyledInput = styled(TextInput)<{ hasInlineHelp?: boolean }>(({
65
+ theme,
66
+ hasInlineHelp
67
+ }) => {
64
68
  const { colour, field } = theme.tokens.components.inputs
65
69
  const typography = field.placeholder.default
66
70
  const { fontFamily } = resolveFont(
@@ -71,6 +75,7 @@ const StyledInput = styled(TextInput)(({ theme }) => {
71
75
  const letterSpacing = typography.letterSpacing?.endsWith("%")
72
76
  ? (parseFloat(typography.letterSpacing) / 100) * fontSize
73
77
  : parseFloat(typography.letterSpacing ?? "0")
78
+
74
79
  return {
75
80
  flex: 1,
76
81
  minWidth: 0,
@@ -80,6 +85,7 @@ const StyledInput = styled(TextInput)(({ theme }) => {
80
85
  fontFamily,
81
86
  fontSize,
82
87
  letterSpacing,
88
+ ...(hasInlineHelp ? { position: "relative", top: -8 } : {}),
83
89
  // Suppress the browser default focus outline on React Native Web —
84
90
  // the focus indicator is rendered on the wrapper instead.
85
91
  ...({
@@ -120,6 +126,7 @@ export const InputField = React.forwardRef<TextInput, InputFieldProps>(
120
126
  hideStateIcons,
121
127
  containerWidth,
122
128
  containerHeight,
129
+ hasInlineHelp,
123
130
  style,
124
131
  editable,
125
132
  onFocus,
@@ -270,6 +277,7 @@ export const InputField = React.forwardRef<TextInput, InputFieldProps>(
270
277
  editable={editable}
271
278
  onFocus={handleFocus}
272
279
  onBlur={handleBlur}
280
+ hasInlineHelp={hasInlineHelp}
273
281
  {...rest}
274
282
  />
275
283
  {trailingIcon && <StyledIconSlot>{trailingIcon}</StyledIconSlot>}
@@ -31,6 +31,10 @@ export default {
31
31
  fieldText: {
32
32
  control: { type: "text" },
33
33
  description: "Unit label shown to the right of the input (e.g. kg, lbs)"
34
+ },
35
+ inlineHelpText: {
36
+ control: { type: "text" },
37
+ description: "Help text displayed below the input field"
34
38
  }
35
39
  }
36
40
  }
@@ -155,6 +159,24 @@ export const CustomStateValidationWithSuccess = () => {
155
159
  )
156
160
  }
157
161
 
162
+ export const WithInlineHelpText = () => (
163
+ <View style={styles.column}>
164
+ <NumberInput
165
+ label="Daily amount"
166
+ placeholder="0"
167
+ inlineHelpText="~ 840 kcals"
168
+ description="Help text"
169
+ />
170
+ <NumberInput
171
+ label="Quantity"
172
+ placeholder="0"
173
+ inlineHelpText="per serving"
174
+ fieldText="portions"
175
+ description="Additional context"
176
+ />
177
+ </View>
178
+ )
179
+
158
180
  export const Controlled = () => {
159
181
  const [value, setValue] = React.useState("")
160
182
 
@@ -13,6 +13,7 @@ type NumberInputOwnProps = Pick<
13
13
  "label" | "description" | "error" | "state" | "optionalText" | "onValueChange"
14
14
  > & {
15
15
  fieldText?: string
16
+ inlineHelpText?: string
16
17
  }
17
18
 
18
19
  export type NumberInputProps = NumberInputOwnProps &
@@ -60,7 +61,8 @@ const StyledRoot = styled(View)(({ theme }) => {
60
61
  * </NumberInput.Root>
61
62
  *
62
63
  * @param {string} [label] - Label text (props API)
63
- * @param {string} [description] - Description/help text (props API)
64
+ * @param {string} [description] - Description/help text below the field (props API)
65
+ * @param {string} [inlineHelpText] - Help text displayed below the input field (props API)
64
66
  * @param {string} [error] - Error message to display (props API, does not affect visual state - use state prop for that)
65
67
  * @param {InputState} [state] - Visual state of the input: 'default', 'error', or 'success' (props API)
66
68
  * @param {string} [optionalText] - Optional text to display next to label (props API)
@@ -77,6 +79,7 @@ const NumberInputRoot = React.forwardRef<View, NumberInputProps>(
77
79
  optionalText,
78
80
  onValueChange,
79
81
  fieldText,
82
+ inlineHelpText,
80
83
  children,
81
84
  ...inputProps
82
85
  },
@@ -97,6 +100,7 @@ const NumberInputRoot = React.forwardRef<View, NumberInputProps>(
97
100
  state={state}
98
101
  onChangeText={onValueChange}
99
102
  fieldText={fieldText}
103
+ inlineHelpText={inlineHelpText}
100
104
  {...inputProps}
101
105
  />
102
106
  {description && (
@@ -21,8 +21,16 @@ const StyledFieldTextWrapper = styled(View)({
21
21
  justifyContent: "center"
22
22
  })
23
23
 
24
+ const StyledInputContainer = styled(View)<{
25
+ hasInlineHelp?: boolean
26
+ }>(({ hasInlineHelp }) => ({
27
+ flexDirection: "column",
28
+ gap: hasInlineHelp ? 2 : 0
29
+ }))
30
+
24
31
  type NumberInputFieldOwnProps = {
25
32
  fieldText?: string
33
+ inlineHelpText?: string
26
34
  }
27
35
 
28
36
  export type NumberInputFieldProps = NumberInputFieldOwnProps &
@@ -36,7 +44,7 @@ export type NumberInputFieldProps = NumberInputFieldOwnProps &
36
44
  export const NumberInputField = React.forwardRef<
37
45
  TextInput,
38
46
  NumberInputFieldProps
39
- >(({ fieldText, ...props }, ref) => {
47
+ >(({ fieldText, inlineHelpText, ...props }, ref) => {
40
48
  const theme = useTheme()
41
49
  const { colour, description } = theme.tokens.components.inputs
42
50
  const fieldSize = parseTokenValue(
@@ -48,30 +56,85 @@ export const NumberInputField = React.forwardRef<
48
56
  keyboardType="numeric"
49
57
  hideStateIcons
50
58
  containerWidth={fieldSize}
51
- containerHeight={fieldSize}
59
+ containerHeight={inlineHelpText ? undefined : fieldSize}
60
+ hasInlineHelp={!!inlineHelpText}
52
61
  ref={ref}
53
62
  style={{ textAlign: "center" }}
54
63
  {...props}
55
64
  />
56
65
  )
57
66
 
58
- if (!fieldText) {
59
- return inputElement
60
- }
61
-
62
- return (
63
- <StyledFieldGroup>
64
- {inputElement}
65
- <StyledFieldTextWrapper>
66
- <Typography
67
- token={description.text.default}
68
- color={colour.description.default}
67
+ const fieldContent =
68
+ inlineHelpText && fieldText ? (
69
+ <StyledFieldGroup>
70
+ <StyledInputContainer hasInlineHelp>
71
+ {inputElement}
72
+ <View
73
+ style={{
74
+ position: "absolute",
75
+ bottom: 4,
76
+ left: 0,
77
+ width: fieldSize,
78
+ display: "flex",
79
+ justifyContent: "center",
80
+ alignItems: "center"
81
+ }}
82
+ >
83
+ <Typography
84
+ token={description.text.default}
85
+ color={colour.description.default}
86
+ >
87
+ {inlineHelpText}
88
+ </Typography>
89
+ </View>
90
+ </StyledInputContainer>
91
+ <StyledFieldTextWrapper>
92
+ <Typography
93
+ token={description.text.default}
94
+ color={colour.description.default}
95
+ >
96
+ {fieldText}
97
+ </Typography>
98
+ </StyledFieldTextWrapper>
99
+ </StyledFieldGroup>
100
+ ) : fieldText ? (
101
+ <StyledFieldGroup>
102
+ {inputElement}
103
+ <StyledFieldTextWrapper>
104
+ <Typography
105
+ token={description.text.default}
106
+ color={colour.description.default}
107
+ >
108
+ {fieldText}
109
+ </Typography>
110
+ </StyledFieldTextWrapper>
111
+ </StyledFieldGroup>
112
+ ) : inlineHelpText ? (
113
+ <StyledInputContainer hasInlineHelp>
114
+ {inputElement}
115
+ <View
116
+ style={{
117
+ position: "absolute",
118
+ bottom: 4,
119
+ width: fieldSize,
120
+ display: "flex",
121
+ justifyContent: "center",
122
+ alignItems: "center"
123
+ }}
69
124
  >
70
- {fieldText}
71
- </Typography>
72
- </StyledFieldTextWrapper>
73
- </StyledFieldGroup>
74
- )
125
+ <Typography
126
+ token={description.text.default}
127
+ color={colour.description.default}
128
+ >
129
+ {inlineHelpText}
130
+ </Typography>
131
+ </View>
132
+ </StyledInputContainer>
133
+ ) : (
134
+ inputElement
135
+ )
136
+
137
+ return fieldContent
75
138
  })
76
139
 
77
140
  NumberInputField.displayName = "NumberInput.Field"
@@ -1,7 +1,7 @@
1
1
  import React from "react"
2
2
  import { screen } from "@testing-library/react"
3
3
  import { describe, it, expect } from "vitest"
4
- import { renderWithTheme } from "../../../test-utils"
4
+ import { renderWithTheme, theme } from "../../../test-utils"
5
5
  import { Typography } from "./Typography"
6
6
 
7
7
  describe("Typography", () => {
@@ -64,7 +64,7 @@ describe("Typography", () => {
64
64
  fontSize: "16",
65
65
  lineHeight: "24"
66
66
  }}
67
- color="#000000"
67
+ color={theme.tokens.semantics.colour.text.primary}
68
68
  >
69
69
  Token text
70
70
  </Typography>
@@ -129,34 +129,36 @@ export type DatePickerProps = DatePickerOwnProps &
129
129
  Omit<ViewProps, keyof DatePickerOwnProps>
130
130
 
131
131
  const StyledContainer = styled(View)<{
132
- containerGap: number
133
132
  containerMaxWidth: number
134
- }>(({ containerGap, containerMaxWidth }) => ({
133
+ }>(({ containerMaxWidth }) => ({
135
134
  flexDirection: "column",
136
- gap: containerGap,
135
+ // Fill the parent by default (capped by maxWidth) so consumers don't need a
136
+ // wrapper. Overridable via the `style` prop.
137
+ width: "100%",
137
138
  maxWidth: containerMaxWidth
138
139
  }))
139
140
 
140
- const StyledMonthHeader = styled(View)<{ headerGap: number }>(
141
- ({ headerGap }) => ({
141
+ const StyledMonthHeader = styled(View)<{ headerGap: number; datesGap: number }>(
142
+ ({ headerGap, datesGap }) => ({
142
143
  flexDirection: "row",
143
144
  alignItems: "center",
144
- gap: headerGap
145
+ gap: headerGap,
146
+ // Gap between the month header and the dates grid.
147
+ marginBottom: datesGap
145
148
  })
146
149
  )
147
150
 
148
- const StyledWeekRow = styled(View)({
149
- flexDirection: "row"
150
- })
151
+ const StyledWeekRow = styled(View)<{ weekGap: number }>(({ weekGap }) => ({
152
+ flexDirection: "row",
153
+ // Gap between the week-day header and the month header.
154
+ marginBottom: weekGap
155
+ }))
151
156
 
152
- const StyledWeekCell = styled(View)<{ cellHeight: number }>(
153
- ({ cellHeight }) => ({
154
- flex: 1,
155
- alignItems: "center",
156
- justifyContent: "center",
157
- height: cellHeight
158
- })
159
- )
157
+ const StyledWeekCell = styled(View)({
158
+ flex: 1,
159
+ alignItems: "center",
160
+ justifyContent: "center"
161
+ })
160
162
 
161
163
  const StyledDatesGrid = styled(View)<{ rowGap: number }>(({ rowGap }) => ({
162
164
  flexDirection: "column",
@@ -280,7 +282,8 @@ export const DatePicker = React.forwardRef<View, DatePickerProps>(
280
282
  const [slideDir, setSlideDir] = useState<"forward" | "backward">("forward")
281
283
 
282
284
  const {
283
- containerGap,
285
+ weekGap,
286
+ datesGap,
284
287
  containerMaxWidth,
285
288
  headerGap,
286
289
  cellHeight,
@@ -291,7 +294,10 @@ export const DatePicker = React.forwardRef<View, DatePickerProps>(
291
294
  rowGap
292
295
  } = useMemo(
293
296
  () => ({
294
- containerGap: parseTokenValue(datePicker.spacing.gap),
297
+ // Gap between the week-day header and the month header.
298
+ weekGap: parseTokenValue(datePicker.spacing.gap),
299
+ // Gap between the month header and the dates grid.
300
+ datesGap: parseTokenValue(theme.tokens.semantics.dimensions.spacing.md),
295
301
  containerMaxWidth: parseTokenValue(dt.size.width) * 7,
296
302
  headerGap: parseTokenValue(datePicker.month.spacing.gap),
297
303
  cellHeight: parseTokenValue(dt.size.height),
@@ -301,7 +307,7 @@ export const DatePicker = React.forwardRef<View, DatePickerProps>(
301
307
  disabledOpacity: parseTokenValue(dt.opacity.disabled),
302
308
  rowGap: parseTokenValue(datePicker.dates.spacing.gap)
303
309
  }),
304
- [datePicker, dt]
310
+ [datePicker, dt, theme]
305
311
  )
306
312
 
307
313
  const resolvedGetDateState = useCallback(
@@ -423,14 +429,13 @@ export const DatePicker = React.forwardRef<View, DatePickerProps>(
423
429
  return (
424
430
  <StyledContainer
425
431
  ref={ref}
426
- containerGap={containerGap}
427
432
  containerMaxWidth={containerMaxWidth}
428
433
  {...rest}
429
434
  >
430
435
  {showWeekHeader && (
431
- <StyledWeekRow accessibilityElementsHidden>
436
+ <StyledWeekRow weekGap={weekGap} accessibilityElementsHidden>
432
437
  {weekDayLabels.map((label) => (
433
- <StyledWeekCell key={label} cellHeight={cellHeight}>
438
+ <StyledWeekCell key={label}>
434
439
  <Typography
435
440
  token={datePicker.week.typography}
436
441
  align="center"
@@ -443,12 +448,12 @@ export const DatePicker = React.forwardRef<View, DatePickerProps>(
443
448
  </StyledWeekRow>
444
449
  )}
445
450
 
446
- <StyledMonthHeader headerGap={headerGap}>
451
+ <StyledMonthHeader headerGap={headerGap} datesGap={datesGap}>
447
452
  <View style={{ opacity: showPrevControl ? 1 : 0 }}>
448
453
  <IconButton
449
454
  icon={KeyboardArrowLeft}
450
455
  variant="filled"
451
- size="sm"
456
+ size="md"
452
457
  colour="primary"
453
458
  aria-label="Previous month"
454
459
  onPress={showPrevControl ? handlePrevMonth : undefined}
@@ -470,7 +475,7 @@ export const DatePicker = React.forwardRef<View, DatePickerProps>(
470
475
  <IconButton
471
476
  icon={KeyboardArrowRight}
472
477
  variant="filled"
473
- size="sm"
478
+ size="md"
474
479
  colour="primary"
475
480
  aria-label="Next month"
476
481
  onPress={showNextControl ? handleNextMonth : undefined}
@@ -321,6 +321,116 @@ export const WithTopContent = () => {
321
321
  )
322
322
  }
323
323
 
324
+ /**
325
+ * Exercises the panel re-measuring its height when body content changes.
326
+ * Use "Add"/"Remove" to grow and shrink the content: the drawer should re-fit
327
+ * its height each time, and once the content exceeds ~90% of the screen it
328
+ * should cap and the body should scroll instead of growing further.
329
+ */
330
+ export const DynamicContent = () => {
331
+ const [count, setCount] = useState(1)
332
+
333
+ return (
334
+ <View style={styles.container}>
335
+ <Drawer.Root defaultOpen>
336
+ <Drawer.Trigger>
337
+ <Button variant="filled" colour="primary">
338
+ Open resizable drawer
339
+ </Button>
340
+ </Drawer.Trigger>
341
+ <Drawer.Portal>
342
+ <Drawer.Overlay />
343
+ <Drawer.Content>
344
+ <Drawer.Header variant="titleAndText">
345
+ <Drawer.Title>Resizable content</Drawer.Title>
346
+ <Drawer.Description>{`${count} paragraph${
347
+ count === 1 ? "" : "s"
348
+ }`}</Drawer.Description>
349
+ <Drawer.Close />
350
+ </Drawer.Header>
351
+ <Drawer.Body>
352
+ {Array.from({ length: count }, (_, i) => (
353
+ <Typography
354
+ key={i}
355
+ >{`Paragraph ${i + 1}: ${bodyText}`}</Typography>
356
+ ))}
357
+ </Drawer.Body>
358
+ <Drawer.Footer>
359
+ <View style={styles.row}>
360
+ <Button
361
+ variant="filled"
362
+ colour="primary"
363
+ onPress={() => setCount((c) => c + 1)}
364
+ >
365
+ Add
366
+ </Button>
367
+ <Button
368
+ variant="outlined"
369
+ colour="secondary"
370
+ onPress={() => setCount((c) => Math.max(0, c - 1))}
371
+ >
372
+ Remove
373
+ </Button>
374
+ </View>
375
+ </Drawer.Footer>
376
+ </Drawer.Content>
377
+ </Drawer.Portal>
378
+ </Drawer.Root>
379
+ </View>
380
+ )
381
+ }
382
+
383
+ /**
384
+ * Simulates async content loading: the drawer opens compact with a loading
385
+ * message, then re-fits taller once the "loaded" content arrives. Tap "Reload"
386
+ * to replay.
387
+ */
388
+ export const AsyncContent = () => {
389
+ const [loaded, setLoaded] = useState(false)
390
+
391
+ const load = () => {
392
+ setLoaded(false)
393
+ setTimeout(() => setLoaded(true), 1200)
394
+ }
395
+
396
+ return (
397
+ <View style={styles.container}>
398
+ <Drawer.Root onOpenChange={(open) => open && load()}>
399
+ <Drawer.Trigger>
400
+ <Button variant="filled" colour="primary">
401
+ Open async drawer
402
+ </Button>
403
+ </Drawer.Trigger>
404
+ <Drawer.Portal>
405
+ <Drawer.Overlay />
406
+ <Drawer.Content>
407
+ <Drawer.Header variant="titleAndText">
408
+ <Drawer.Title>Delivery details</Drawer.Title>
409
+ <Drawer.Close />
410
+ </Drawer.Header>
411
+ <Drawer.Body>
412
+ {loaded ? (
413
+ Array.from({ length: 5 }, (_, i) => (
414
+ <Typography
415
+ key={i}
416
+ >{`Detail ${i + 1}: ${bodyText}`}</Typography>
417
+ ))
418
+ ) : (
419
+ <Typography>Loading…</Typography>
420
+ )}
421
+ </Drawer.Body>
422
+ <Drawer.Footer>
423
+ <Button variant="filled" colour="primary" onPress={load}>
424
+ Reload
425
+ </Button>
426
+ </Drawer.Footer>
427
+ </Drawer.Content>
428
+ </Drawer.Portal>
429
+ </Drawer.Root>
430
+ </View>
431
+ )
432
+ }
433
+
324
434
  const styles = StyleSheet.create({
325
435
  container: {
326
436
  padding: 16
@@ -1,9 +1,14 @@
1
- import React from "react"
2
- import { ScrollView, ScrollViewProps, useWindowDimensions } from "react-native"
1
+ import React, { useCallback } from "react"
2
+ import {
3
+ type LayoutChangeEvent,
4
+ ScrollView,
5
+ ScrollViewProps
6
+ } from "react-native"
3
7
  import styled from "@emotion/native"
4
8
  import { useTheme } from "@emotion/react"
5
9
  import { useDrawerHeaderContext } from "./DrawerHeaderContext"
6
10
  import { useDrawerFooterContext } from "./DrawerFooterContext"
11
+ import { useDrawerMeasureContext } from "./DrawerMeasureContext"
7
12
 
8
13
  export type DrawerBodyProps = ScrollViewProps
9
14
 
@@ -13,21 +18,22 @@ const StyledScrollView = styled(ScrollView)<{
13
18
  bodyPaddingHorizontal: number
14
19
  bodyPaddingRight: number
15
20
  bodyGap: number
16
- bodyMaxHeight: number
17
21
  bodyPaddingBottom: number
18
22
  }>(
19
23
  ({
20
24
  bodyPaddingHorizontal,
21
25
  bodyPaddingRight,
22
26
  bodyGap,
23
- bodyMaxHeight,
24
27
  bodyPaddingBottom
25
28
  }) => ({
29
+ // flex: 1 fills the bounded panel so the ScrollView can scroll. The panel
30
+ // (DrawerContent) is the single height authority — the body must NOT set
31
+ // its own maxHeight, or it would cap independently of the panel and corrupt
32
+ // the panel's chrome measurement.
26
33
  flex: 1,
27
34
  paddingLeft: bodyPaddingHorizontal,
28
35
  paddingRight: bodyPaddingRight,
29
36
  gap: bodyGap,
30
- maxHeight: bodyMaxHeight,
31
37
  paddingBottom: bodyPaddingBottom
32
38
  })
33
39
  )
@@ -37,19 +43,45 @@ const StyledScrollView = styled(ScrollView)<{
37
43
  * between the header and footer.
38
44
  */
39
45
  export const DrawerBody = React.forwardRef<ScrollView, DrawerBodyProps>(
40
- ({ children, contentContainerStyle, ...props }, ref) => {
46
+ (
47
+ {
48
+ children,
49
+ contentContainerStyle,
50
+ onLayout,
51
+ onContentSizeChange,
52
+ ...props
53
+ },
54
+ ref
55
+ ) => {
41
56
  const theme = useTheme()
42
57
  const { spacing } = theme.tokens.components.drawer
43
58
  const { buttons } = theme.tokens.components
44
59
  const { content } = spacing
45
60
  const headerContext = useDrawerHeaderContext()
46
61
  const footerContext = useDrawerFooterContext()
47
- const { height: windowHeight } = useWindowDimensions()
62
+ const measureContext = useDrawerMeasureContext()
48
63
 
49
64
  const gap = parseTokenValue(content.slot.gap)
50
65
  const horizontalPadding = parseTokenValue(content.slot.horizontalPadding)
51
66
  const topPadding = parseTokenValue(content.slot.verticalPadding)
52
- const bodyMaxHeight = windowHeight - 300
67
+
68
+ // Report the ScrollView's frame and content heights so DrawerContent can
69
+ // re-fit the panel when the body content changes.
70
+ const handleLayout = useCallback(
71
+ (event: LayoutChangeEvent) => {
72
+ measureContext?.setBodyFrameHeight(event.nativeEvent.layout.height)
73
+ onLayout?.(event)
74
+ },
75
+ [measureContext, onLayout]
76
+ )
77
+
78
+ const handleContentSizeChange = useCallback(
79
+ (width: number, height: number) => {
80
+ measureContext?.setBodyContentHeight(height)
81
+ onContentSizeChange?.(width, height)
82
+ },
83
+ [measureContext, onContentSizeChange]
84
+ )
53
85
 
54
86
  // When there's no header the close button floats in the top-right corner.
55
87
  // Reserve space on the right so body content doesn't slide under it.
@@ -69,8 +101,9 @@ export const DrawerBody = React.forwardRef<ScrollView, DrawerBodyProps>(
69
101
  bodyPaddingHorizontal={horizontalPadding}
70
102
  bodyPaddingRight={paddingRight}
71
103
  bodyGap={gap}
72
- bodyMaxHeight={bodyMaxHeight}
73
104
  bodyPaddingBottom={bodyPaddingBottom}
105
+ onLayout={handleLayout}
106
+ onContentSizeChange={handleContentSizeChange}
74
107
  contentContainerStyle={[
75
108
  {
76
109
  gap,