@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
|
@@ -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)
|
|
@@ -25,6 +25,10 @@ export default {
|
|
|
25
25
|
control: { type: "text" },
|
|
26
26
|
description: "Help text displayed below the field"
|
|
27
27
|
},
|
|
28
|
+
inlineHelpText: {
|
|
29
|
+
control: { type: "text" },
|
|
30
|
+
description: "Help text displayed inside the input field"
|
|
31
|
+
},
|
|
28
32
|
error: {
|
|
29
33
|
control: { type: "text" },
|
|
30
34
|
description: "Error message when state is error"
|
|
@@ -221,6 +225,25 @@ export const FullWidth = () => {
|
|
|
221
225
|
)
|
|
222
226
|
}
|
|
223
227
|
|
|
228
|
+
export const WithInlineHelpText = () => {
|
|
229
|
+
const [value, setValue] = useState(12)
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<View style={styles.section}>
|
|
233
|
+
<NumberField
|
|
234
|
+
label="Daily amount"
|
|
235
|
+
size="lg"
|
|
236
|
+
inlineHelpText="~ 840 kcals"
|
|
237
|
+
description="Help text"
|
|
238
|
+
value={String(value)}
|
|
239
|
+
onChangeText={(t) => setValue(Number(t) || 0)}
|
|
240
|
+
onIncrement={() => setValue((v) => v + 1)}
|
|
241
|
+
onDecrement={() => setValue((v) => Math.max(0, v - 1))}
|
|
242
|
+
/>
|
|
243
|
+
</View>
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
224
247
|
export const Playground = {
|
|
225
248
|
args: {
|
|
226
249
|
size: "lg",
|
|
@@ -5,6 +5,7 @@ import { useTheme } from "@emotion/react"
|
|
|
5
5
|
import { IconButton } from "../../atoms/IconButton"
|
|
6
6
|
import { Add, Remove } from "@butternutbox/pawprint-icons/core"
|
|
7
7
|
import { resolveFont } from "../../../fonts"
|
|
8
|
+
import { Typography } from "../../atoms/Typography"
|
|
8
9
|
|
|
9
10
|
type NumberFieldSize = "sm" | "lg"
|
|
10
11
|
|
|
@@ -21,6 +22,7 @@ type NumberFieldInputOwnProps = {
|
|
|
21
22
|
decrementDisabled?: boolean
|
|
22
23
|
min?: number
|
|
23
24
|
max?: number
|
|
25
|
+
inlineHelpText?: string
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
export type NumberFieldInputProps = NumberFieldInputOwnProps &
|
|
@@ -48,6 +50,7 @@ const StyledFieldWrapper = styled(View)<{
|
|
|
48
50
|
}) => ({
|
|
49
51
|
alignItems: "center",
|
|
50
52
|
justifyContent: "center",
|
|
53
|
+
position: "relative",
|
|
51
54
|
width: fullWidth ? "100%" : fieldMinWidth,
|
|
52
55
|
minWidth: fieldMinWidth,
|
|
53
56
|
height: fieldHeight,
|
|
@@ -79,19 +82,29 @@ const StyledTextInput = styled(TextInput)<{
|
|
|
79
82
|
inputFontFamily: string
|
|
80
83
|
inputFontWeight?: FontWeight
|
|
81
84
|
inputFontSize: number
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
85
|
+
hasInlineHelp?: boolean
|
|
86
|
+
}>(
|
|
87
|
+
({
|
|
88
|
+
inputColor,
|
|
89
|
+
inputFontFamily,
|
|
90
|
+
inputFontWeight,
|
|
91
|
+
inputFontSize,
|
|
92
|
+
hasInlineHelp
|
|
93
|
+
}) => ({
|
|
94
|
+
textAlign: "center" as const,
|
|
95
|
+
outlineStyle: "none" as unknown as undefined,
|
|
96
|
+
padding: 0,
|
|
97
|
+
minWidth: 0,
|
|
98
|
+
minHeight: 0,
|
|
99
|
+
width: "100%",
|
|
100
|
+
height: "100%",
|
|
101
|
+
color: inputColor,
|
|
102
|
+
fontFamily: inputFontFamily,
|
|
103
|
+
...(inputFontWeight ? { fontWeight: inputFontWeight } : {}),
|
|
104
|
+
fontSize: inputFontSize,
|
|
105
|
+
...(hasInlineHelp ? { position: "relative", top: -8 } : {})
|
|
106
|
+
})
|
|
107
|
+
)
|
|
95
108
|
|
|
96
109
|
const StyledRow = styled(View)<{
|
|
97
110
|
rowGap: number
|
|
@@ -106,6 +119,8 @@ const StyledRow = styled(View)<{
|
|
|
106
119
|
* Renders decrement button, centered numeric input, increment button,
|
|
107
120
|
* and optional side text (e.g. "kg").
|
|
108
121
|
*
|
|
122
|
+
* @param {string} [inlineHelpText] - Help text displayed inside the input field at the bottom
|
|
123
|
+
*
|
|
109
124
|
* Border states:
|
|
110
125
|
* - default: `colour.field.border.default`
|
|
111
126
|
* - focused: `colour.field.border.selected`
|
|
@@ -130,6 +145,7 @@ export const NumberFieldInput = React.forwardRef<
|
|
|
130
145
|
decrementDisabled = false,
|
|
131
146
|
min,
|
|
132
147
|
max,
|
|
148
|
+
inlineHelpText,
|
|
133
149
|
style,
|
|
134
150
|
onFocus,
|
|
135
151
|
onBlur,
|
|
@@ -252,6 +268,7 @@ export const NumberFieldInput = React.forwardRef<
|
|
|
252
268
|
: undefined
|
|
253
269
|
}
|
|
254
270
|
inputFontSize={parseTokenValue(typographyToken.fontSize)}
|
|
271
|
+
hasInlineHelp={!!inlineHelpText}
|
|
255
272
|
keyboardType="numeric"
|
|
256
273
|
editable={!disabled}
|
|
257
274
|
onFocus={handleFocus}
|
|
@@ -262,6 +279,25 @@ export const NumberFieldInput = React.forwardRef<
|
|
|
262
279
|
style={style}
|
|
263
280
|
{...rest}
|
|
264
281
|
/>
|
|
282
|
+
{inlineHelpText && (
|
|
283
|
+
<View
|
|
284
|
+
style={{
|
|
285
|
+
position: "absolute",
|
|
286
|
+
bottom: 4,
|
|
287
|
+
width: fieldMinWidth,
|
|
288
|
+
display: "flex",
|
|
289
|
+
justifyContent: "center",
|
|
290
|
+
alignItems: "center"
|
|
291
|
+
}}
|
|
292
|
+
>
|
|
293
|
+
<Typography
|
|
294
|
+
token={inputTokens.description.text.default}
|
|
295
|
+
color={inputTokens.colour.description.default}
|
|
296
|
+
>
|
|
297
|
+
{inlineHelpText}
|
|
298
|
+
</Typography>
|
|
299
|
+
</View>
|
|
300
|
+
)}
|
|
265
301
|
</StyledFieldWrapper>
|
|
266
302
|
{showIncrementButton && (
|
|
267
303
|
<IconButton
|
|
@@ -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
|
}))
|