@butternutbox/pawprint-native 0.10.9 → 0.10.11
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 +4 -4
- package/CHANGELOG.md +36 -0
- package/dist/index.cjs +85 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +85 -18
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/molecules/Drawer/DrawerBody.tsx +15 -2
- package/src/components/molecules/Drawer/DrawerContent.tsx +132 -42
- package/src/components/molecules/Drawer/DrawerMeasureContext.ts +18 -0
- package/src/components/molecules/NumberField/NumberField.tsx +5 -1
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import React from "react"
|
|
1
|
+
import React, { useCallback } from "react"
|
|
2
2
|
import { ScrollView, ScrollViewProps, useWindowDimensions } from "react-native"
|
|
3
3
|
import styled from "@emotion/native"
|
|
4
4
|
import { useTheme } from "@emotion/react"
|
|
5
5
|
import { useDrawerHeaderContext } from "./DrawerHeaderContext"
|
|
6
6
|
import { useDrawerFooterContext } from "./DrawerFooterContext"
|
|
7
|
+
import { useDrawerMeasureContext } from "./DrawerMeasureContext"
|
|
7
8
|
|
|
8
9
|
export type DrawerBodyProps = ScrollViewProps
|
|
9
10
|
|
|
@@ -37,15 +38,26 @@ const StyledScrollView = styled(ScrollView)<{
|
|
|
37
38
|
* between the header and footer.
|
|
38
39
|
*/
|
|
39
40
|
export const DrawerBody = React.forwardRef<ScrollView, DrawerBodyProps>(
|
|
40
|
-
({ children, contentContainerStyle, ...props }, ref) => {
|
|
41
|
+
({ children, contentContainerStyle, onContentSizeChange, ...props }, ref) => {
|
|
41
42
|
const theme = useTheme()
|
|
42
43
|
const { spacing } = theme.tokens.components.drawer
|
|
43
44
|
const { buttons } = theme.tokens.components
|
|
44
45
|
const { content } = spacing
|
|
45
46
|
const headerContext = useDrawerHeaderContext()
|
|
46
47
|
const footerContext = useDrawerFooterContext()
|
|
48
|
+
const measureContext = useDrawerMeasureContext()
|
|
47
49
|
const { height: windowHeight } = useWindowDimensions()
|
|
48
50
|
|
|
51
|
+
// Report the scroll content's natural height so DrawerContent can re-fit
|
|
52
|
+
// the panel when the body grows or shrinks.
|
|
53
|
+
const handleContentSizeChange = useCallback(
|
|
54
|
+
(width: number, height: number): void => {
|
|
55
|
+
measureContext?.setBodyContentHeight(height)
|
|
56
|
+
onContentSizeChange?.(width, height)
|
|
57
|
+
},
|
|
58
|
+
[measureContext, onContentSizeChange]
|
|
59
|
+
)
|
|
60
|
+
|
|
49
61
|
const gap = parseTokenValue(content.slot.gap)
|
|
50
62
|
const horizontalPadding = parseTokenValue(content.slot.horizontalPadding)
|
|
51
63
|
const topPadding = parseTokenValue(content.slot.verticalPadding)
|
|
@@ -71,6 +83,7 @@ export const DrawerBody = React.forwardRef<ScrollView, DrawerBodyProps>(
|
|
|
71
83
|
bodyGap={gap}
|
|
72
84
|
bodyMaxHeight={bodyMaxHeight}
|
|
73
85
|
bodyPaddingBottom={bodyPaddingBottom}
|
|
86
|
+
onContentSizeChange={handleContentSizeChange}
|
|
74
87
|
contentContainerStyle={[
|
|
75
88
|
{
|
|
76
89
|
gap,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useMemo, useRef } from "react"
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|
2
2
|
import {
|
|
3
3
|
Animated,
|
|
4
4
|
Easing,
|
|
@@ -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 {
|
|
@@ -35,7 +36,7 @@ const StyledPanel = styled(View)<{
|
|
|
35
36
|
panelShadowColor: string
|
|
36
37
|
panelShadowOffsetY: number
|
|
37
38
|
panelShadowBlur: number
|
|
38
|
-
|
|
39
|
+
panelFlex?: number
|
|
39
40
|
}>(
|
|
40
41
|
({
|
|
41
42
|
panelBorderRadius,
|
|
@@ -46,17 +47,12 @@ const StyledPanel = styled(View)<{
|
|
|
46
47
|
panelShadowColor,
|
|
47
48
|
panelShadowOffsetY,
|
|
48
49
|
panelShadowBlur,
|
|
49
|
-
|
|
50
|
+
panelFlex
|
|
50
51
|
}) => ({
|
|
51
52
|
width: "100%",
|
|
52
53
|
maxWidth: panelMaxWidth,
|
|
53
|
-
// The panel wraps its content and caps at maxHeight. It is intentionally
|
|
54
|
-
// NOT given a fixed height: letting it size to content means it re-fits
|
|
55
|
-
// automatically whenever the body grows or shrinks (async load, expanding
|
|
56
|
-
// sections). The body's own maxHeight (see DrawerBody) bounds scrolling and
|
|
57
|
-
// keeps the ScrollView from collapsing, so no fixed panel height is needed.
|
|
58
|
-
maxHeight: panelMaxHeight,
|
|
59
54
|
alignSelf: "center",
|
|
55
|
+
...(panelFlex != null ? { flex: panelFlex } : undefined),
|
|
60
56
|
borderTopLeftRadius: panelBorderRadius,
|
|
61
57
|
borderTopRightRadius: panelBorderRadius,
|
|
62
58
|
borderTopWidth: panelBorderWidth,
|
|
@@ -78,6 +74,14 @@ const StyledPanel = styled(View)<{
|
|
|
78
74
|
* the screen. Uses React Native `Animated` for entrance/exit and `PanResponder`
|
|
79
75
|
* on the grabber for drag-to-dismiss.
|
|
80
76
|
*
|
|
77
|
+
* The panel is given an explicit height (needed so DrawerBody's `flex: 1`
|
|
78
|
+
* ScrollView has a bounded parent and doesn't collapse). That height re-fits
|
|
79
|
+
* when the body content changes: DrawerBody reports its natural scroll-content
|
|
80
|
+
* height, and the panel is sized to `chrome + min(content, bodyMax)` (capped at
|
|
81
|
+
* 90% of the window). `chrome` (header + footer + padding + borders) is
|
|
82
|
+
* measured once from the first, unbounded layout and is invariant thereafter,
|
|
83
|
+
* so there is no measurement feedback loop.
|
|
84
|
+
*
|
|
81
85
|
* Must be rendered inside `Drawer.Portal`.
|
|
82
86
|
*
|
|
83
87
|
* @example
|
|
@@ -104,6 +108,10 @@ export const DrawerContent = React.forwardRef<View, DrawerContentProps>(
|
|
|
104
108
|
|
|
105
109
|
const { height: windowHeight } = useWindowDimensions()
|
|
106
110
|
const maxHeight = windowHeight * 0.9
|
|
111
|
+
// Tallest the body content grows before it scrolls. Kept in sync with the
|
|
112
|
+
// `maxHeight` DrawerBody sets on its own ScrollView, so the panel's re-fit
|
|
113
|
+
// cap and the body's scroll cap agree.
|
|
114
|
+
const bodyMaxHeight = windowHeight - 300
|
|
107
115
|
|
|
108
116
|
// Keep a ref so the PanResponder (created once) always calls the latest
|
|
109
117
|
// closeDrawer without becoming a stale closure.
|
|
@@ -119,6 +127,19 @@ export const DrawerContent = React.forwardRef<View, DrawerContentProps>(
|
|
|
119
127
|
const isOpenRef = useRef(isOpen)
|
|
120
128
|
const activeAnim = useRef<Animated.CompositeAnimation | null>(null)
|
|
121
129
|
|
|
130
|
+
// Explicit panel height (capped at maxHeight). Applied to the wrapper +
|
|
131
|
+
// flex:1 on the panel so DrawerBody's ScrollView gets a bounded container
|
|
132
|
+
// and can scroll correctly.
|
|
133
|
+
const [panelHeight, setPanelHeight] = useState(0)
|
|
134
|
+
|
|
135
|
+
// Re-fit inputs. `chrome` is the invariant non-body height (header + footer
|
|
136
|
+
// + padding + borders); `bodyContent` is the body's natural content height;
|
|
137
|
+
// `firstNatural` is the panel's natural height at the first unbounded layout
|
|
138
|
+
// (before an explicit height is applied), used to derive chrome exactly.
|
|
139
|
+
const chromeRef = useRef(0)
|
|
140
|
+
const bodyContentRef = useRef(0)
|
|
141
|
+
const firstNaturalRef = useRef(0)
|
|
142
|
+
|
|
122
143
|
useEffect(() => {
|
|
123
144
|
const id = translateY.addListener(({ value }) => {
|
|
124
145
|
translateYValue.current = value
|
|
@@ -126,6 +147,15 @@ export const DrawerContent = React.forwardRef<View, DrawerContentProps>(
|
|
|
126
147
|
return () => translateY.removeListener(id)
|
|
127
148
|
}, [translateY])
|
|
128
149
|
|
|
150
|
+
// Clamp the cached panel height when the window shrinks (e.g. orientation
|
|
151
|
+
// change) so the entry animation never starts from below the screen.
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
if (panelHeightRef.current > 0 && panelHeightRef.current > maxHeight) {
|
|
154
|
+
setPanelHeight(maxHeight)
|
|
155
|
+
panelHeightRef.current = maxHeight
|
|
156
|
+
}
|
|
157
|
+
}, [maxHeight])
|
|
158
|
+
|
|
129
159
|
const runEntry = useCallback(() => {
|
|
130
160
|
if (panelHeightRef.current === 0) return
|
|
131
161
|
translateY.setValue(panelHeightRef.current)
|
|
@@ -173,25 +203,79 @@ export const DrawerContent = React.forwardRef<View, DrawerContentProps>(
|
|
|
173
203
|
}
|
|
174
204
|
}, [isOpen, runEntry, runExit])
|
|
175
205
|
|
|
206
|
+
// Desired panel height = chrome + body content (clamped to bodyMax, then
|
|
207
|
+
// maxHeight). Only sets state when it actually changes, to avoid loops.
|
|
208
|
+
const applyRefit = useCallback(() => {
|
|
209
|
+
const chrome = chromeRef.current
|
|
210
|
+
const content = bodyContentRef.current
|
|
211
|
+
if (chrome <= 0 || content <= 0) return
|
|
212
|
+
const desired = Math.min(
|
|
213
|
+
chrome + Math.min(content, bodyMaxHeight),
|
|
214
|
+
maxHeight
|
|
215
|
+
)
|
|
216
|
+
if (Math.abs(desired - panelHeightRef.current) > 1) {
|
|
217
|
+
panelHeightRef.current = desired
|
|
218
|
+
setPanelHeight(desired)
|
|
219
|
+
}
|
|
220
|
+
}, [bodyMaxHeight, maxHeight])
|
|
221
|
+
|
|
222
|
+
// Lock chrome once, from the first unbounded layout: there the panel wraps
|
|
223
|
+
// its content, so natural height = chrome + min(content, bodyMax). chrome is
|
|
224
|
+
// layout-invariant, so it never needs re-deriving (which is what made the
|
|
225
|
+
// earlier frame-subtraction approach fragile).
|
|
226
|
+
const lockChrome = useCallback(() => {
|
|
227
|
+
if (chromeRef.current > 0) return
|
|
228
|
+
const natural = firstNaturalRef.current
|
|
229
|
+
const content = bodyContentRef.current
|
|
230
|
+
if (natural <= 0 || content <= 0) return
|
|
231
|
+
chromeRef.current = natural - Math.min(content, bodyMaxHeight)
|
|
232
|
+
}, [bodyMaxHeight])
|
|
233
|
+
|
|
234
|
+
const setBodyContentHeight = useCallback(
|
|
235
|
+
(height: number) => {
|
|
236
|
+
if (height <= 0) return
|
|
237
|
+
bodyContentRef.current = height
|
|
238
|
+
lockChrome()
|
|
239
|
+
applyRefit()
|
|
240
|
+
},
|
|
241
|
+
[lockChrome, applyRefit]
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
const measureContextValue = useMemo(
|
|
245
|
+
() => ({ setBodyContentHeight }),
|
|
246
|
+
[setBodyContentHeight]
|
|
247
|
+
)
|
|
248
|
+
|
|
176
249
|
const onLayout = useCallback(
|
|
177
250
|
({ nativeEvent }: { nativeEvent: { layout: { height: number } } }) => {
|
|
178
251
|
const h = nativeEvent.layout.height
|
|
179
252
|
const capped = Math.min(h, maxHeight)
|
|
180
253
|
if (capped <= 0) return
|
|
181
254
|
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
|
|
255
|
+
// First measurement only: the panel is still wrapping its content
|
|
256
|
+
// (no explicit height yet), so `h` is the natural height. Capture it to
|
|
257
|
+
// derive chrome, fix the panel height, and run the entry animation.
|
|
258
|
+
// Afterwards the panel echoes its fixed height, so re-fitting is driven
|
|
259
|
+
// solely by the body's content size (setBodyContentHeight).
|
|
260
|
+
if (entryRanRef.current) return
|
|
187
261
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
262
|
+
if (chromeRef.current <= 0) {
|
|
263
|
+
firstNaturalRef.current = h
|
|
264
|
+
lockChrome()
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (capped !== panelHeightRef.current) {
|
|
268
|
+
panelHeightRef.current = capped
|
|
269
|
+
setPanelHeight(capped)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (isOpenRef.current) {
|
|
191
273
|
runEntry()
|
|
192
274
|
}
|
|
275
|
+
|
|
276
|
+
applyRefit()
|
|
193
277
|
},
|
|
194
|
-
[runEntry, maxHeight]
|
|
278
|
+
[runEntry, maxHeight, lockChrome, applyRefit]
|
|
195
279
|
)
|
|
196
280
|
|
|
197
281
|
const panResponder = useRef(
|
|
@@ -278,31 +362,37 @@ export const DrawerContent = React.forwardRef<View, DrawerContentProps>(
|
|
|
278
362
|
return (
|
|
279
363
|
<DrawerHeaderContext.Provider value={headerContextValue}>
|
|
280
364
|
<DrawerFooterContext.Provider value={hasFooter}>
|
|
281
|
-
<
|
|
282
|
-
<
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
panelBgColor={colour.background.container.default}
|
|
290
|
-
panelMaxWidth={parseTokenValue(drawer.size.maxWidth)}
|
|
291
|
-
panelMaxHeight={maxHeight}
|
|
292
|
-
panelBorderWidth={parseTokenValue(dimensions.borderWidth.sm)}
|
|
293
|
-
panelBorderColor={colour.border.default}
|
|
294
|
-
panelShadowColor={modal.shadow.color}
|
|
295
|
-
panelShadowOffsetY={parseTokenValue(modal.shadow.offsetY)}
|
|
296
|
-
panelShadowBlur={parseTokenValue(modal.shadow.blur)}
|
|
297
|
-
accessible
|
|
298
|
-
accessibilityRole="none"
|
|
299
|
-
accessibilityViewIsModal
|
|
300
|
-
{...props}
|
|
365
|
+
<DrawerMeasureContext.Provider value={measureContextValue}>
|
|
366
|
+
<DrawerDragContext.Provider value={dragContextValue}>
|
|
367
|
+
<Animated.View
|
|
368
|
+
style={[
|
|
369
|
+
styles.animatedWrapper,
|
|
370
|
+
panelHeight > 0 ? { height: panelHeight } : undefined,
|
|
371
|
+
{ transform: [{ translateY }] }
|
|
372
|
+
]}
|
|
301
373
|
>
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
374
|
+
<StyledPanel
|
|
375
|
+
ref={ref}
|
|
376
|
+
onLayout={onLayout}
|
|
377
|
+
panelBorderRadius={parseTokenValue(drawer.borderRadius.top)}
|
|
378
|
+
panelBgColor={colour.background.container.default}
|
|
379
|
+
panelMaxWidth={parseTokenValue(drawer.size.maxWidth)}
|
|
380
|
+
panelFlex={panelHeight > 0 ? 1 : undefined}
|
|
381
|
+
panelBorderWidth={parseTokenValue(dimensions.borderWidth.sm)}
|
|
382
|
+
panelBorderColor={colour.border.default}
|
|
383
|
+
panelShadowColor={modal.shadow.color}
|
|
384
|
+
panelShadowOffsetY={parseTokenValue(modal.shadow.offsetY)}
|
|
385
|
+
panelShadowBlur={parseTokenValue(modal.shadow.blur)}
|
|
386
|
+
accessible
|
|
387
|
+
accessibilityRole="none"
|
|
388
|
+
accessibilityViewIsModal
|
|
389
|
+
{...props}
|
|
390
|
+
>
|
|
391
|
+
{children}
|
|
392
|
+
</StyledPanel>
|
|
393
|
+
</Animated.View>
|
|
394
|
+
</DrawerDragContext.Provider>
|
|
395
|
+
</DrawerMeasureContext.Provider>
|
|
306
396
|
</DrawerFooterContext.Provider>
|
|
307
397
|
</DrawerHeaderContext.Provider>
|
|
308
398
|
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createContext, useContext } from "react"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lets `DrawerBody` report its scroll content's natural height up to
|
|
5
|
+
* `DrawerContent`, which uses it to re-fit the panel when the content grows or
|
|
6
|
+
* shrinks after open. A ScrollView's content size is measured in an unbounded
|
|
7
|
+
* container, so it reflects the true content height independently of the
|
|
8
|
+
* ScrollView's own (bounded) frame — no measurement feedback loop.
|
|
9
|
+
*/
|
|
10
|
+
export type DrawerMeasureContextValue = {
|
|
11
|
+
setBodyContentHeight: (height: number) => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const DrawerMeasureContext =
|
|
15
|
+
createContext<DrawerMeasureContextValue | null>(null)
|
|
16
|
+
|
|
17
|
+
export const useDrawerMeasureContext = (): DrawerMeasureContextValue | null =>
|
|
18
|
+
useContext(DrawerMeasureContext)
|
|
@@ -41,7 +41,7 @@ const StyledRoot = styled(View)<{
|
|
|
41
41
|
fullWidth?: boolean
|
|
42
42
|
}>(({ rootGap, fullWidth }) => ({
|
|
43
43
|
gap: rootGap,
|
|
44
|
-
...(fullWidth
|
|
44
|
+
...(fullWidth ? { width: "100%" } : { alignItems: "center" })
|
|
45
45
|
}))
|
|
46
46
|
|
|
47
47
|
const StyledLabelGroup = styled(View)<{
|
|
@@ -134,6 +134,7 @@ export const NumberField = React.forwardRef<View, NumberFieldProps>(
|
|
|
134
134
|
<Typography
|
|
135
135
|
token={typoTokens.label}
|
|
136
136
|
color={tokens.colour.text.label}
|
|
137
|
+
align="center"
|
|
137
138
|
>
|
|
138
139
|
{label}
|
|
139
140
|
</Typography>
|
|
@@ -142,6 +143,7 @@ export const NumberField = React.forwardRef<View, NumberFieldProps>(
|
|
|
142
143
|
<Typography
|
|
143
144
|
token={typoTokens.smallLabel}
|
|
144
145
|
color={tokens.colour.text.description}
|
|
146
|
+
align="center"
|
|
145
147
|
>
|
|
146
148
|
{smallLabel}
|
|
147
149
|
</Typography>
|
|
@@ -172,6 +174,7 @@ export const NumberField = React.forwardRef<View, NumberFieldProps>(
|
|
|
172
174
|
: tokens.typography.small.smallLabel
|
|
173
175
|
}
|
|
174
176
|
color={tokens.colour.text.description}
|
|
177
|
+
align="center"
|
|
175
178
|
>
|
|
176
179
|
{description}
|
|
177
180
|
</Typography>
|
|
@@ -184,6 +187,7 @@ export const NumberField = React.forwardRef<View, NumberFieldProps>(
|
|
|
184
187
|
: tokens.typography.small.smallLabel
|
|
185
188
|
}
|
|
186
189
|
color={theme.tokens.components.inputs.colour.description.error}
|
|
190
|
+
align="center"
|
|
187
191
|
>
|
|
188
192
|
{error}
|
|
189
193
|
</Typography>
|