@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/.turbo/turbo-build.log +9 -9
- package/CHANGELOG.md +14 -0
- package/dist/index.cjs +959 -773
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +472 -286
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/atoms/Illustration/Illustration.stories.tsx +3 -1
- package/src/components/atoms/Input/InputField.tsx +9 -1
- package/src/components/atoms/NumberInput/NumberInput.stories.tsx +22 -0
- package/src/components/atoms/NumberInput/NumberInput.tsx +5 -1
- package/src/components/atoms/NumberInput/NumberInputField.tsx +81 -18
- package/src/components/atoms/Typography/Typography.test.tsx +2 -2
- package/src/components/molecules/DatePicker/DatePicker.tsx +31 -26
- package/src/components/molecules/Drawer/Drawer.stories.tsx +110 -0
- package/src/components/molecules/Drawer/DrawerBody.tsx +42 -9
- package/src/components/molecules/Drawer/DrawerContent.tsx +101 -38
- package/src/components/molecules/Drawer/DrawerMeasureContext.ts +22 -0
- package/src/components/molecules/NumberField/NumberField.stories.tsx +23 -0
- package/src/components/molecules/NumberField/NumberFieldInput.tsx +49 -13
- package/src/components/molecules/Radio/Radio.tsx +3 -0
package/package.json
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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=
|
|
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
|
-
}>(({
|
|
133
|
+
}>(({ containerMaxWidth }) => ({
|
|
135
134
|
flexDirection: "column",
|
|
136
|
-
|
|
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)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
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="
|
|
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="
|
|
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 {
|
|
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
|
-
(
|
|
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
|
|
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
|
-
|
|
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,
|