@butternutbox/pawprint-native 0.10.7 → 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 +5 -5
- package/CHANGELOG.md +8 -0
- package/dist/index.cjs +811 -729
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +324 -242
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- 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/Radio/Radio.tsx +3 -0
package/package.json
CHANGED
|
@@ -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,
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import styled from "@emotion/native"
|
|
12
12
|
import { useTheme } from "@emotion/react"
|
|
13
13
|
import { DrawerDragContext } from "./DrawerDragContext"
|
|
14
|
+
import { DrawerMeasureContext } from "./DrawerMeasureContext"
|
|
14
15
|
import { useDrawerContext } from "./DrawerContext"
|
|
15
16
|
import { DrawerFooterContext } from "./DrawerFooterContext"
|
|
16
17
|
import {
|
|
@@ -99,6 +100,10 @@ export const DrawerContent = React.forwardRef<View, DrawerContentProps>(
|
|
|
99
100
|
|
|
100
101
|
const { height: windowHeight } = useWindowDimensions()
|
|
101
102
|
const maxHeight = windowHeight * 0.9
|
|
103
|
+
// Tallest the body content is allowed to grow before it scrolls. Enforced
|
|
104
|
+
// here (via the panel height) rather than as a maxHeight on the body
|
|
105
|
+
// ScrollView, so the panel stays the single height authority.
|
|
106
|
+
const bodyMaxHeight = windowHeight - 300
|
|
102
107
|
|
|
103
108
|
// Keep a ref so the PanResponder (created once) always calls the latest
|
|
104
109
|
// closeDrawer without becoming a stale closure.
|
|
@@ -185,29 +190,85 @@ export const DrawerContent = React.forwardRef<View, DrawerContentProps>(
|
|
|
185
190
|
|
|
186
191
|
const onLayout = useCallback(
|
|
187
192
|
({ nativeEvent }: { nativeEvent: { layout: { height: number } } }) => {
|
|
193
|
+
// This only bootstraps the first measurement and the entry animation.
|
|
194
|
+
// After entry has run, applyDesiredHeight (driven by the body's
|
|
195
|
+
// content size) is the SOLE authority for panel height — this handler
|
|
196
|
+
// must not keep overriding it, or the two fight and the panel gets
|
|
197
|
+
// pinned to chrome + bodyFrame (leaving a gap) instead of
|
|
198
|
+
// chrome + content.
|
|
199
|
+
if (entryRanRef.current) return
|
|
200
|
+
|
|
188
201
|
const h = nativeEvent.layout.height
|
|
189
202
|
const capped = Math.min(h, maxHeight)
|
|
203
|
+
if (capped <= 0) return
|
|
190
204
|
|
|
191
|
-
|
|
192
|
-
// avoid re-render loops. Compare against the ref (kept in sync with
|
|
193
|
-
// state) so panelHeight doesn't need to be a dep of this callback.
|
|
194
|
-
if (capped !== panelHeightRef.current && capped > 0) {
|
|
205
|
+
if (capped !== panelHeightRef.current) {
|
|
195
206
|
setPanelHeight(capped)
|
|
196
207
|
panelHeightRef.current = capped
|
|
197
208
|
}
|
|
198
209
|
|
|
199
|
-
|
|
200
|
-
// capped height even though the panel may still be at natural size;
|
|
201
|
-
// the re-render that sets the explicit height happens while the panel
|
|
202
|
-
// is still off-screen, so there is no visible jump.
|
|
203
|
-
if (isOpenRef.current && !entryRanRef.current && capped > 0) {
|
|
204
|
-
panelHeightRef.current = capped
|
|
210
|
+
if (isOpenRef.current) {
|
|
205
211
|
runEntry()
|
|
206
212
|
}
|
|
207
213
|
},
|
|
208
214
|
[runEntry, maxHeight]
|
|
209
215
|
)
|
|
210
216
|
|
|
217
|
+
// Re-fit the panel height whenever the body content changes. Desired height
|
|
218
|
+
// is the absolute `chrome + content` (clamped to bodyMax, then maxHeight).
|
|
219
|
+
// `chrome` (header + footer + padding + borders) is invariant for a given
|
|
220
|
+
// drawer, so it is measured ONCE and locked. Deriving it repeatedly from
|
|
221
|
+
// `panelHeight - bodyFrame` is unsafe: those two are only consistent at a
|
|
222
|
+
// settled layout, and mid-resize the frame lags, which inflates chrome and
|
|
223
|
+
// pins the panel to chrome + frame (a gap) instead of chrome + content.
|
|
224
|
+
const chromeRef = useRef(0)
|
|
225
|
+
const bodyContentRef = useRef(0)
|
|
226
|
+
|
|
227
|
+
const applyDesiredHeight = useCallback(() => {
|
|
228
|
+
const chrome = chromeRef.current
|
|
229
|
+
const content = bodyContentRef.current
|
|
230
|
+
const current = panelHeightRef.current
|
|
231
|
+
if (chrome <= 0 || content <= 0 || current <= 0) return
|
|
232
|
+
const desired = Math.min(
|
|
233
|
+
chrome + Math.min(content, bodyMaxHeight),
|
|
234
|
+
maxHeight
|
|
235
|
+
)
|
|
236
|
+
if (Math.abs(desired - current) > 1) {
|
|
237
|
+
panelHeightRef.current = desired
|
|
238
|
+
setPanelHeight(desired)
|
|
239
|
+
}
|
|
240
|
+
}, [maxHeight, bodyMaxHeight])
|
|
241
|
+
|
|
242
|
+
const setBodyFrameHeight = useCallback(
|
|
243
|
+
(frameHeight: number) => {
|
|
244
|
+
// Lock chrome once, from the first settled measurement (initial layout,
|
|
245
|
+
// before any content toggling), where panelHeight and bodyFrame are
|
|
246
|
+
// consistent. It stays fixed thereafter.
|
|
247
|
+
if (chromeRef.current <= 0) {
|
|
248
|
+
const current = panelHeightRef.current
|
|
249
|
+
if (frameHeight > 0 && current > frameHeight) {
|
|
250
|
+
chromeRef.current = current - frameHeight
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
applyDesiredHeight()
|
|
254
|
+
},
|
|
255
|
+
[applyDesiredHeight]
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
const setBodyContentHeight = useCallback(
|
|
259
|
+
(contentHeight: number) => {
|
|
260
|
+
if (contentHeight <= 0) return
|
|
261
|
+
bodyContentRef.current = contentHeight
|
|
262
|
+
applyDesiredHeight()
|
|
263
|
+
},
|
|
264
|
+
[applyDesiredHeight]
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
const measureContextValue = useMemo(
|
|
268
|
+
() => ({ setBodyFrameHeight, setBodyContentHeight }),
|
|
269
|
+
[setBodyFrameHeight, setBodyContentHeight]
|
|
270
|
+
)
|
|
271
|
+
|
|
211
272
|
const panResponder = useRef(
|
|
212
273
|
PanResponder.create({
|
|
213
274
|
// Claim the responder as soon as a touch starts on the grabber so the
|
|
@@ -292,35 +353,37 @@ export const DrawerContent = React.forwardRef<View, DrawerContentProps>(
|
|
|
292
353
|
return (
|
|
293
354
|
<DrawerHeaderContext.Provider value={headerContextValue}>
|
|
294
355
|
<DrawerFooterContext.Provider value={hasFooter}>
|
|
295
|
-
<
|
|
296
|
-
<
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
<StyledPanel
|
|
304
|
-
ref={ref}
|
|
305
|
-
onLayout={onLayout}
|
|
306
|
-
panelBorderRadius={parseTokenValue(drawer.borderRadius.top)}
|
|
307
|
-
panelBgColor={colour.background.container.default}
|
|
308
|
-
panelMaxWidth={parseTokenValue(drawer.size.maxWidth)}
|
|
309
|
-
panelFlex={panelHeight > 0 ? 1 : undefined}
|
|
310
|
-
panelBorderWidth={parseTokenValue(dimensions.borderWidth.sm)}
|
|
311
|
-
panelBorderColor={colour.border.default}
|
|
312
|
-
panelShadowColor={modal.shadow.color}
|
|
313
|
-
panelShadowOffsetY={parseTokenValue(modal.shadow.offsetY)}
|
|
314
|
-
panelShadowBlur={parseTokenValue(modal.shadow.blur)}
|
|
315
|
-
accessible
|
|
316
|
-
accessibilityRole="none"
|
|
317
|
-
accessibilityViewIsModal
|
|
318
|
-
{...props}
|
|
356
|
+
<DrawerMeasureContext.Provider value={measureContextValue}>
|
|
357
|
+
<DrawerDragContext.Provider value={dragContextValue}>
|
|
358
|
+
<Animated.View
|
|
359
|
+
style={[
|
|
360
|
+
styles.animatedWrapper,
|
|
361
|
+
panelHeight > 0 ? { height: panelHeight } : undefined,
|
|
362
|
+
{ transform: [{ translateY }] }
|
|
363
|
+
]}
|
|
319
364
|
>
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
365
|
+
<StyledPanel
|
|
366
|
+
ref={ref}
|
|
367
|
+
onLayout={onLayout}
|
|
368
|
+
panelBorderRadius={parseTokenValue(drawer.borderRadius.top)}
|
|
369
|
+
panelBgColor={colour.background.container.default}
|
|
370
|
+
panelMaxWidth={parseTokenValue(drawer.size.maxWidth)}
|
|
371
|
+
panelFlex={panelHeight > 0 ? 1 : undefined}
|
|
372
|
+
panelBorderWidth={parseTokenValue(dimensions.borderWidth.sm)}
|
|
373
|
+
panelBorderColor={colour.border.default}
|
|
374
|
+
panelShadowColor={modal.shadow.color}
|
|
375
|
+
panelShadowOffsetY={parseTokenValue(modal.shadow.offsetY)}
|
|
376
|
+
panelShadowBlur={parseTokenValue(modal.shadow.blur)}
|
|
377
|
+
accessible
|
|
378
|
+
accessibilityRole="none"
|
|
379
|
+
accessibilityViewIsModal
|
|
380
|
+
{...props}
|
|
381
|
+
>
|
|
382
|
+
{children}
|
|
383
|
+
</StyledPanel>
|
|
384
|
+
</Animated.View>
|
|
385
|
+
</DrawerDragContext.Provider>
|
|
386
|
+
</DrawerMeasureContext.Provider>
|
|
324
387
|
</DrawerFooterContext.Provider>
|
|
325
388
|
</DrawerHeaderContext.Provider>
|
|
326
389
|
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
|
|
3
|
+
export type DrawerMeasureContextValue = {
|
|
4
|
+
/**
|
|
5
|
+
* The drawer body's ScrollView frame height (its laid-out size). Together
|
|
6
|
+
* with the panel height this yields the fixed "chrome" (header + footer +
|
|
7
|
+
* padding), since panel height always equals chrome + body frame.
|
|
8
|
+
*/
|
|
9
|
+
setBodyFrameHeight: (height: number) => void
|
|
10
|
+
/**
|
|
11
|
+
* The drawer body's content height (full scrollable content). Drives the
|
|
12
|
+
* panel's desired height as `chrome + content`, so the drawer re-fits when
|
|
13
|
+
* content changes.
|
|
14
|
+
*/
|
|
15
|
+
setBodyContentHeight: (height: number) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const DrawerMeasureContext =
|
|
19
|
+
React.createContext<DrawerMeasureContextValue | null>(null)
|
|
20
|
+
|
|
21
|
+
export const useDrawerMeasureContext = () =>
|
|
22
|
+
React.useContext(DrawerMeasureContext)
|
|
@@ -96,6 +96,9 @@ const StyledIndicator = styled(View)<{
|
|
|
96
96
|
const StyledTextContent = styled(View)<{
|
|
97
97
|
textContentGap: number
|
|
98
98
|
}>(({ textContentGap }) => ({
|
|
99
|
+
// Take the row's remaining width so long label/subText wraps within the tile
|
|
100
|
+
// instead of pushing the trailing asset past the tile's edge.
|
|
101
|
+
flex: 1,
|
|
99
102
|
flexDirection: "column",
|
|
100
103
|
gap: textContentGap
|
|
101
104
|
}))
|