@butternutbox/pawprint-native 2.2.0 → 2.3.0
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 +21 -0
- package/COMPONENT_GUIDELINES.md +12 -8
- package/dist/index.cjs +232 -116
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +161 -45
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__mocks__/react-native.tsx +10 -1
- package/src/components/atoms/IconButton/IconButton.tsx +1 -0
- package/src/components/molecules/DatePicker/DatePicker.tsx +32 -2
- package/src/components/molecules/Drawer/Drawer.test.tsx +63 -0
- package/src/components/molecules/Drawer/DrawerBody.tsx +131 -25
- package/src/components/molecules/Drawer/DrawerContent.tsx +9 -6
- package/src/components/molecules/Drawer/DrawerKeyboardInsetContext.ts +9 -0
- package/src/components/molecules/SelectField/SelectField.tsx +13 -3
package/package.json
CHANGED
|
@@ -205,6 +205,9 @@ export const TextInput = React.forwardRef<HTMLInputElement, any>(
|
|
|
205
205
|
}
|
|
206
206
|
)
|
|
207
207
|
TextInput.displayName = "TextInput"
|
|
208
|
+
;(TextInput as unknown as { State: unknown }).State = {
|
|
209
|
+
currentlyFocusedInput: () => null
|
|
210
|
+
}
|
|
208
211
|
|
|
209
212
|
// ─── ScrollView ──────────────────────────────────────────────────────────
|
|
210
213
|
export const ScrollView = React.forwardRef<HTMLDivElement, any>(
|
|
@@ -216,11 +219,16 @@ export const ScrollView = React.forwardRef<HTMLDivElement, any>(
|
|
|
216
219
|
showsHorizontalScrollIndicator: _showsHorizontalScrollIndicator,
|
|
217
220
|
showsVerticalScrollIndicator: _showsVerticalScrollIndicator,
|
|
218
221
|
nestedScrollEnabled: _nestedScrollEnabled,
|
|
222
|
+
keyboardShouldPersistTaps,
|
|
219
223
|
..._rest
|
|
220
224
|
},
|
|
221
225
|
ref
|
|
222
226
|
) => (
|
|
223
|
-
<div
|
|
227
|
+
<div
|
|
228
|
+
ref={ref}
|
|
229
|
+
style={contentContainerStyle}
|
|
230
|
+
data-keyboard-should-persist-taps={keyboardShouldPersistTaps}
|
|
231
|
+
>
|
|
224
232
|
{children}
|
|
225
233
|
</div>
|
|
226
234
|
)
|
|
@@ -310,6 +318,7 @@ export const Animated = {
|
|
|
310
318
|
)
|
|
311
319
|
}
|
|
312
320
|
),
|
|
321
|
+
ScrollView,
|
|
313
322
|
Value: AnimatedValue,
|
|
314
323
|
timing: (_value: any, _config: any) => ({
|
|
315
324
|
start: mockAnim,
|
|
@@ -176,6 +176,7 @@ const IconButton = React.forwardRef<View, IconButtonProps>(
|
|
|
176
176
|
<StyledIconButton
|
|
177
177
|
ref={ref}
|
|
178
178
|
disabled={isDisabled}
|
|
179
|
+
accessibilityRole="button"
|
|
179
180
|
accessibilityLabel={ariaLabel}
|
|
180
181
|
accessibilityState={{ disabled: isDisabled, busy: loading }}
|
|
181
182
|
onPressIn={() => setPressed(true)}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import React, { useCallback, useMemo, useState } from "react"
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|
2
2
|
import { format, isBefore, isSameDay, isToday, type Locale } from "date-fns"
|
|
3
|
-
import { View, Pressable, ViewProps } from "react-native"
|
|
3
|
+
import { AccessibilityInfo, View, Pressable, ViewProps } from "react-native"
|
|
4
4
|
import styled from "@emotion/native"
|
|
5
5
|
import { useTheme } from "@emotion/react"
|
|
6
6
|
import {
|
|
@@ -52,6 +52,10 @@ const MONTH_SLIDE_EXITING = {
|
|
|
52
52
|
backward: FadeOutRight.duration(200)
|
|
53
53
|
} as const
|
|
54
54
|
|
|
55
|
+
// Moving screen-reader focus straight after a tap or a sheet closing is
|
|
56
|
+
// ignored by VoiceOver, so wait for it to settle first.
|
|
57
|
+
const FOCUS_DELAY_MS = 150
|
|
58
|
+
|
|
55
59
|
const DEFAULT_WEEK_LABELS: [
|
|
56
60
|
string,
|
|
57
61
|
string,
|
|
@@ -127,6 +131,7 @@ type DatePickerOwnProps = {
|
|
|
127
131
|
monthLabel: string
|
|
128
132
|
) => string
|
|
129
133
|
isDateDisabled?: (date: Date) => boolean
|
|
134
|
+
focusDate?: Date | null
|
|
130
135
|
minDate?: Date
|
|
131
136
|
maxDate?: Date
|
|
132
137
|
}
|
|
@@ -236,6 +241,8 @@ const StyledIndicator = styled(View)<{
|
|
|
236
241
|
* Defaults to: past="previous", today="current", future="default".
|
|
237
242
|
* @param {(date: Date, state: DateItemState, monthLabel: string) => string} [getDateAccessibilityLabel] - Returns the screen-reader label for a date,
|
|
238
243
|
* e.g. to announce delivery states. Defaults to "{day} {monthLabel}".
|
|
244
|
+
* @param {Date | null} [focusDate] - Moves screen-reader focus to this date's cell when it is set or
|
|
245
|
+
* changes, e.g. to return focus to a picked date after a confirmation sheet closes.
|
|
239
246
|
* @param {(date: Date) => boolean} [isDateDisabled] - Returns true to disable a date.
|
|
240
247
|
* @param {Date} [minDate] - Dates before this (exclusive of time) will be disabled.
|
|
241
248
|
* @param {Date} [maxDate] - Dates after this (exclusive of time) will be disabled.
|
|
@@ -279,6 +286,7 @@ export const DatePicker = React.forwardRef<View, DatePickerProps>(
|
|
|
279
286
|
getDateState,
|
|
280
287
|
getDateAccessibilityLabel,
|
|
281
288
|
isDateDisabled,
|
|
289
|
+
focusDate,
|
|
282
290
|
minDate,
|
|
283
291
|
maxDate,
|
|
284
292
|
...rest
|
|
@@ -320,6 +328,23 @@ export const DatePicker = React.forwardRef<View, DatePickerProps>(
|
|
|
320
328
|
[datePicker, dt, theme]
|
|
321
329
|
)
|
|
322
330
|
|
|
331
|
+
const cellRefs = useRef(new Map<string, View>())
|
|
332
|
+
const focusTime = focusDate?.getTime()
|
|
333
|
+
|
|
334
|
+
useEffect(() => {
|
|
335
|
+
if (focusTime === undefined) return
|
|
336
|
+
|
|
337
|
+
const target = new Date(focusTime)
|
|
338
|
+
if (target.getFullYear() !== year || target.getMonth() !== month) return
|
|
339
|
+
|
|
340
|
+
const timeout = setTimeout(() => {
|
|
341
|
+
const cell = cellRefs.current.get(toDateKey(target))
|
|
342
|
+
if (cell) AccessibilityInfo.sendAccessibilityEvent(cell, "focus")
|
|
343
|
+
}, FOCUS_DELAY_MS)
|
|
344
|
+
|
|
345
|
+
return () => clearTimeout(timeout)
|
|
346
|
+
}, [focusTime, year, month])
|
|
347
|
+
|
|
323
348
|
const resolvedGetDateState = useCallback(
|
|
324
349
|
(date: Date) => getDateState?.(date) ?? defaultGetDateState(date),
|
|
325
350
|
[getDateState]
|
|
@@ -544,6 +569,11 @@ export const DatePicker = React.forwardRef<View, DatePickerProps>(
|
|
|
544
569
|
|
|
545
570
|
return (
|
|
546
571
|
<Pressable
|
|
572
|
+
ref={(node) => {
|
|
573
|
+
const key = toDateKey(date)
|
|
574
|
+
if (node) cellRefs.current.set(key, node)
|
|
575
|
+
else cellRefs.current.delete(key)
|
|
576
|
+
}}
|
|
547
577
|
onPress={
|
|
548
578
|
!isDisabled
|
|
549
579
|
? () => onDateSelect?.(date)
|
|
@@ -90,6 +90,69 @@ describe("Drawer keyboard lift", () => {
|
|
|
90
90
|
|
|
91
91
|
expect(screen.getByText("Row one")).toBeInTheDocument()
|
|
92
92
|
})
|
|
93
|
+
|
|
94
|
+
it("keeps its content once the keyboard has settled", () => {
|
|
95
|
+
renderWithTheme(
|
|
96
|
+
<Drawer.Root defaultOpen>
|
|
97
|
+
<Drawer.Portal>
|
|
98
|
+
<Drawer.Content>
|
|
99
|
+
<Drawer.Body>
|
|
100
|
+
<Typography>Row one</Typography>
|
|
101
|
+
</Drawer.Body>
|
|
102
|
+
</Drawer.Content>
|
|
103
|
+
</Drawer.Portal>
|
|
104
|
+
</Drawer.Root>
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
act(() => {
|
|
108
|
+
Keyboard.emit("keyboardDidShow", {
|
|
109
|
+
endCoordinates: { height: 336 },
|
|
110
|
+
duration: 250
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
expect(screen.getByText("Row one")).toBeInTheDocument()
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe("Drawer.Body taps", () => {
|
|
119
|
+
const persistTapsOf = (text: string) =>
|
|
120
|
+
screen
|
|
121
|
+
.getByText(text)
|
|
122
|
+
.closest("[data-keyboard-should-persist-taps]")
|
|
123
|
+
?.getAttribute("data-keyboard-should-persist-taps")
|
|
124
|
+
|
|
125
|
+
it("lets taps reach its controls without dismissing the keyboard", () => {
|
|
126
|
+
renderWithTheme(
|
|
127
|
+
<Drawer.Root defaultOpen>
|
|
128
|
+
<Drawer.Portal>
|
|
129
|
+
<Drawer.Content>
|
|
130
|
+
<Drawer.Body>
|
|
131
|
+
<Typography>Row one</Typography>
|
|
132
|
+
</Drawer.Body>
|
|
133
|
+
</Drawer.Content>
|
|
134
|
+
</Drawer.Portal>
|
|
135
|
+
</Drawer.Root>
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
expect(persistTapsOf("Row one")).toBe("handled")
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it("respects a consumer's keyboardShouldPersistTaps", () => {
|
|
142
|
+
renderWithTheme(
|
|
143
|
+
<Drawer.Root defaultOpen>
|
|
144
|
+
<Drawer.Portal>
|
|
145
|
+
<Drawer.Content>
|
|
146
|
+
<Drawer.Body keyboardShouldPersistTaps="never">
|
|
147
|
+
<Typography>Row one</Typography>
|
|
148
|
+
</Drawer.Body>
|
|
149
|
+
</Drawer.Content>
|
|
150
|
+
</Drawer.Portal>
|
|
151
|
+
</Drawer.Root>
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
expect(persistTapsOf("Row one")).toBe("never")
|
|
155
|
+
})
|
|
93
156
|
})
|
|
94
157
|
|
|
95
158
|
describe("Drawer", () => {
|
|
@@ -1,46 +1,62 @@
|
|
|
1
|
-
import React from "react"
|
|
2
|
-
import {
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef } from "react"
|
|
2
|
+
import {
|
|
3
|
+
Animated,
|
|
4
|
+
Keyboard,
|
|
5
|
+
ScrollView,
|
|
6
|
+
ScrollViewProps,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
TextInput,
|
|
9
|
+
useWindowDimensions,
|
|
10
|
+
View,
|
|
11
|
+
type LayoutChangeEvent,
|
|
12
|
+
type NativeScrollEvent,
|
|
13
|
+
type NativeSyntheticEvent
|
|
14
|
+
} from "react-native"
|
|
3
15
|
import styled from "@emotion/native"
|
|
4
16
|
import { useTheme } from "@emotion/react"
|
|
5
17
|
import { useSafeAreaInsets } from "react-native-safe-area-context"
|
|
6
18
|
import { useDrawerHeaderContext } from "./DrawerHeaderContext"
|
|
7
19
|
import { useDrawerFooterContext } from "./DrawerFooterContext"
|
|
8
20
|
import { useDrawerBodyMax } from "./DrawerBodyMaxContext"
|
|
21
|
+
import { useDrawerKeyboardInset } from "./DrawerKeyboardInsetContext"
|
|
9
22
|
|
|
10
23
|
export type DrawerBodyProps = ScrollViewProps
|
|
11
24
|
|
|
12
25
|
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
13
26
|
|
|
14
|
-
const StyledScrollView = styled(ScrollView)<{
|
|
27
|
+
const StyledScrollView = styled(Animated.ScrollView)<{
|
|
15
28
|
bodyPaddingHorizontal: number
|
|
16
29
|
bodyPaddingRight: number
|
|
17
|
-
bodyMaxHeight: number
|
|
18
30
|
bodyPaddingBottom: number
|
|
19
|
-
}>(
|
|
20
|
-
(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
flexGrow: 0,
|
|
30
|
-
flexShrink: 1,
|
|
31
|
-
paddingLeft: bodyPaddingHorizontal,
|
|
32
|
-
paddingRight: bodyPaddingRight,
|
|
33
|
-
maxHeight: bodyMaxHeight,
|
|
34
|
-
paddingBottom: bodyPaddingBottom
|
|
35
|
-
})
|
|
36
|
-
)
|
|
31
|
+
}>(({ bodyPaddingHorizontal, bodyPaddingRight, bodyPaddingBottom }) => ({
|
|
32
|
+
// No `flex: 1`: the ScrollView self-sizes to its content (up to
|
|
33
|
+
// `maxHeight`), so the panel wraps tight around short content and scrolls
|
|
34
|
+
// only when content exceeds the chrome-aware cap.
|
|
35
|
+
flexGrow: 0,
|
|
36
|
+
flexShrink: 1,
|
|
37
|
+
paddingLeft: bodyPaddingHorizontal,
|
|
38
|
+
paddingRight: bodyPaddingRight,
|
|
39
|
+
paddingBottom: bodyPaddingBottom
|
|
40
|
+
}))
|
|
37
41
|
|
|
38
42
|
/**
|
|
39
43
|
* Scrollable content area of the drawer. Sizes to its content up to a
|
|
40
44
|
* chrome-aware `maxHeight`, scrolling beyond that.
|
|
41
45
|
*/
|
|
42
46
|
export const DrawerBody = React.forwardRef<ScrollView, DrawerBodyProps>(
|
|
43
|
-
(
|
|
47
|
+
(
|
|
48
|
+
{
|
|
49
|
+
children,
|
|
50
|
+
contentContainerStyle,
|
|
51
|
+
keyboardShouldPersistTaps = "handled",
|
|
52
|
+
scrollEventThrottle = 16,
|
|
53
|
+
onScroll,
|
|
54
|
+
onLayout,
|
|
55
|
+
style,
|
|
56
|
+
...props
|
|
57
|
+
},
|
|
58
|
+
ref
|
|
59
|
+
) => {
|
|
44
60
|
const theme = useTheme()
|
|
45
61
|
const { spacing } = theme.tokens.components.drawer
|
|
46
62
|
const { buttons } = theme.tokens.components
|
|
@@ -48,6 +64,7 @@ export const DrawerBody = React.forwardRef<ScrollView, DrawerBodyProps>(
|
|
|
48
64
|
const headerContext = useDrawerHeaderContext()
|
|
49
65
|
const footerContext = useDrawerFooterContext()
|
|
50
66
|
const providedMaxHeight = useDrawerBodyMax()
|
|
67
|
+
const keyboardInset = useDrawerKeyboardInset()
|
|
51
68
|
const { height: windowHeight } = useWindowDimensions()
|
|
52
69
|
const insets = useSafeAreaInsets()
|
|
53
70
|
|
|
@@ -60,6 +77,90 @@ export const DrawerBody = React.forwardRef<ScrollView, DrawerBodyProps>(
|
|
|
60
77
|
// chrome has been measured, so the body still scrolls on the first layout.
|
|
61
78
|
const bodyMaxHeight = providedMaxHeight ?? windowHeight - 300
|
|
62
79
|
|
|
80
|
+
const { maxHeight: styleMaxHeight, ...restStyle } =
|
|
81
|
+
StyleSheet.flatten(style) ?? {}
|
|
82
|
+
const cap =
|
|
83
|
+
typeof styleMaxHeight === "number"
|
|
84
|
+
? Math.min(styleMaxHeight, bodyMaxHeight)
|
|
85
|
+
: bodyMaxHeight
|
|
86
|
+
|
|
87
|
+
const maxHeight = useMemo(() => {
|
|
88
|
+
if (!keyboardInset || bodyMaxHeight <= 0) return cap
|
|
89
|
+
const slack = bodyMaxHeight - cap
|
|
90
|
+
return keyboardInset.interpolate({
|
|
91
|
+
inputRange: slack > 0 ? [0, slack, bodyMaxHeight] : [0, bodyMaxHeight],
|
|
92
|
+
outputRange: slack > 0 ? [cap, cap, 0] : [cap, 0],
|
|
93
|
+
extrapolate: "clamp"
|
|
94
|
+
})
|
|
95
|
+
}, [keyboardInset, bodyMaxHeight, cap])
|
|
96
|
+
|
|
97
|
+
const scrollRef = useRef<ScrollView | null>(null)
|
|
98
|
+
const contentRef = useRef<View>(null)
|
|
99
|
+
const scrollOffsetRef = useRef(0)
|
|
100
|
+
const viewportHeightRef = useRef(0)
|
|
101
|
+
|
|
102
|
+
const combinedRef = useCallback(
|
|
103
|
+
(node: ScrollView | null) => {
|
|
104
|
+
scrollRef.current = node
|
|
105
|
+
if (typeof ref === "function") {
|
|
106
|
+
ref(node)
|
|
107
|
+
} else if (ref) {
|
|
108
|
+
;(ref as React.MutableRefObject<ScrollView | null>).current = node
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
[ref]
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
const handleScroll = useCallback(
|
|
115
|
+
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
116
|
+
scrollOffsetRef.current = event.nativeEvent.contentOffset.y
|
|
117
|
+
onScroll?.(event)
|
|
118
|
+
},
|
|
119
|
+
[onScroll]
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
const handleLayout = useCallback(
|
|
123
|
+
(event: LayoutChangeEvent) => {
|
|
124
|
+
viewportHeightRef.current = event.nativeEvent.layout.height
|
|
125
|
+
onLayout?.(event)
|
|
126
|
+
},
|
|
127
|
+
[onLayout]
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
const subscription = Keyboard.addListener("keyboardDidShow", () => {
|
|
132
|
+
const scrollView = scrollRef.current
|
|
133
|
+
const focusedInput = TextInput.State.currentlyFocusedInput()
|
|
134
|
+
const contentView = contentRef.current
|
|
135
|
+
if (!scrollView || !focusedInput || !contentView) return
|
|
136
|
+
|
|
137
|
+
focusedInput.measureLayout(
|
|
138
|
+
contentView,
|
|
139
|
+
(_left, inputTop, _width, inputHeight) => {
|
|
140
|
+
const viewportHeight = viewportHeightRef.current
|
|
141
|
+
if (viewportHeight <= 0) return
|
|
142
|
+
|
|
143
|
+
const visibleTop = scrollOffsetRef.current
|
|
144
|
+
const visibleBottom = visibleTop + viewportHeight
|
|
145
|
+
const wantedTop = inputTop - gap
|
|
146
|
+
const wantedBottom = inputTop + inputHeight + gap
|
|
147
|
+
|
|
148
|
+
if (wantedTop < visibleTop) {
|
|
149
|
+
scrollView.scrollTo({ y: Math.max(0, wantedTop), animated: true })
|
|
150
|
+
} else if (wantedBottom > visibleBottom) {
|
|
151
|
+
scrollView.scrollTo({
|
|
152
|
+
y: Math.min(wantedTop, wantedBottom - viewportHeight),
|
|
153
|
+
animated: true
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
() => {}
|
|
158
|
+
)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
return () => subscription.remove()
|
|
162
|
+
}, [gap])
|
|
163
|
+
|
|
63
164
|
// When there's no header the close button floats in the top-right corner.
|
|
64
165
|
// Reserve space on the right so body content doesn't slide under it.
|
|
65
166
|
const paddingRight =
|
|
@@ -82,11 +183,12 @@ export const DrawerBody = React.forwardRef<ScrollView, DrawerBodyProps>(
|
|
|
82
183
|
|
|
83
184
|
return (
|
|
84
185
|
<StyledScrollView
|
|
85
|
-
ref={
|
|
186
|
+
ref={combinedRef}
|
|
187
|
+
innerViewRef={contentRef as React.RefObject<View>}
|
|
86
188
|
bodyPaddingHorizontal={horizontalPadding}
|
|
87
189
|
bodyPaddingRight={paddingRight}
|
|
88
|
-
bodyMaxHeight={bodyMaxHeight}
|
|
89
190
|
bodyPaddingBottom={bodyPaddingBottom}
|
|
191
|
+
style={[restStyle, { maxHeight }]}
|
|
90
192
|
contentContainerStyle={[
|
|
91
193
|
{
|
|
92
194
|
gap,
|
|
@@ -95,6 +197,10 @@ export const DrawerBody = React.forwardRef<ScrollView, DrawerBodyProps>(
|
|
|
95
197
|
},
|
|
96
198
|
contentContainerStyle
|
|
97
199
|
]}
|
|
200
|
+
keyboardShouldPersistTaps={keyboardShouldPersistTaps}
|
|
201
|
+
scrollEventThrottle={scrollEventThrottle}
|
|
202
|
+
onScroll={handleScroll}
|
|
203
|
+
onLayout={handleLayout}
|
|
98
204
|
{...props}
|
|
99
205
|
>
|
|
100
206
|
{children}
|
|
@@ -17,6 +17,7 @@ import { DrawerMeasureContext } from "./DrawerMeasureContext"
|
|
|
17
17
|
import { useDrawerContext } from "./DrawerContext"
|
|
18
18
|
import { DrawerBodyMaxContext } from "./DrawerBodyMaxContext"
|
|
19
19
|
import { DrawerFooterContext } from "./DrawerFooterContext"
|
|
20
|
+
import { DrawerKeyboardInsetContext } from "./DrawerKeyboardInsetContext"
|
|
20
21
|
import {
|
|
21
22
|
DrawerHeaderContext,
|
|
22
23
|
type DrawerHeaderVariant
|
|
@@ -31,6 +32,8 @@ const DISMISS_THRESHOLD = 80
|
|
|
31
32
|
|
|
32
33
|
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
33
34
|
|
|
35
|
+
const claimTouch = () => true
|
|
36
|
+
|
|
34
37
|
const StyledPanel = styled(View)<{
|
|
35
38
|
panelBorderRadius: number
|
|
36
39
|
panelBgColor: string
|
|
@@ -111,9 +114,8 @@ export const DrawerContent = React.forwardRef<View, DrawerContentProps>(
|
|
|
111
114
|
const { isOpen, closeDrawer, onExitComplete } = useDrawerContext()
|
|
112
115
|
|
|
113
116
|
const { height: windowHeight } = useWindowDimensions()
|
|
114
|
-
const [keyboardHeight, setKeyboardHeight] = useState(0)
|
|
115
117
|
const keyboardSpacer = useRef(new Animated.Value(0)).current
|
|
116
|
-
const maxHeight =
|
|
118
|
+
const maxHeight = windowHeight * 0.9
|
|
117
119
|
|
|
118
120
|
// Constant parts of the chrome that aren't a measured section: the panel's
|
|
119
121
|
// top border, and (when there's no footer) the body's own bottom padding.
|
|
@@ -178,11 +180,9 @@ export const DrawerContent = React.forwardRef<View, DrawerContentProps>(
|
|
|
178
180
|
}
|
|
179
181
|
|
|
180
182
|
const shown = Keyboard.addListener(showEvent, (event) => {
|
|
181
|
-
setKeyboardHeight(event.endCoordinates.height)
|
|
182
183
|
lift(event.endCoordinates.height, event.duration)
|
|
183
184
|
})
|
|
184
185
|
const hidden = Keyboard.addListener(hideEvent, (event) => {
|
|
185
|
-
setKeyboardHeight(0)
|
|
186
186
|
lift(0, event.duration)
|
|
187
187
|
})
|
|
188
188
|
|
|
@@ -427,7 +427,7 @@ export const DrawerContent = React.forwardRef<View, DrawerContentProps>(
|
|
|
427
427
|
panelBorderRadius={parseTokenValue(drawer.borderRadius.top)}
|
|
428
428
|
panelBgColor={colour.background.container.default}
|
|
429
429
|
panelMaxWidth={parseTokenValue(drawer.size.maxWidth)}
|
|
430
|
-
panelMaxHeight={maxHeight
|
|
430
|
+
panelMaxHeight={maxHeight}
|
|
431
431
|
panelBorderWidth={parseTokenValue(dimensions.borderWidth.sm)}
|
|
432
432
|
panelBorderColor={colour.border.default}
|
|
433
433
|
panelShadowColor={modal.shadow.color}
|
|
@@ -435,10 +435,13 @@ export const DrawerContent = React.forwardRef<View, DrawerContentProps>(
|
|
|
435
435
|
panelShadowBlur={parseTokenValue(modal.shadow.blur)}
|
|
436
436
|
accessibilityViewIsModal
|
|
437
437
|
onAccessibilityEscape={closeDrawer}
|
|
438
|
+
onStartShouldSetResponder={claimTouch}
|
|
438
439
|
{...props}
|
|
439
440
|
>
|
|
440
441
|
<DrawerBodyMaxContext.Provider value={bodyMax}>
|
|
441
|
-
{
|
|
442
|
+
<DrawerKeyboardInsetContext.Provider value={keyboardSpacer}>
|
|
443
|
+
{children}
|
|
444
|
+
</DrawerKeyboardInsetContext.Provider>
|
|
442
445
|
</DrawerBodyMaxContext.Provider>
|
|
443
446
|
<Animated.View style={{ height: keyboardSpacer }} />
|
|
444
447
|
</StyledPanel>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createContext, useContext } from "react"
|
|
2
|
+
import type { Animated } from "react-native"
|
|
3
|
+
|
|
4
|
+
export const DrawerKeyboardInsetContext = createContext<Animated.Value | null>(
|
|
5
|
+
null
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
export const useDrawerKeyboardInset = (): Animated.Value | null =>
|
|
9
|
+
useContext(DrawerKeyboardInsetContext)
|
|
@@ -180,9 +180,20 @@ export const SelectField = React.forwardRef<View, SelectFieldProps>(
|
|
|
180
180
|
)
|
|
181
181
|
}, [windowHeight, keyboardHeight, maxDropdownHeight])
|
|
182
182
|
|
|
183
|
+
const isOpen = isFocused && !disabled
|
|
184
|
+
|
|
183
185
|
React.useEffect(() => {
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
+
if (!isOpen) return
|
|
187
|
+
|
|
188
|
+
let frame = 0
|
|
189
|
+
const track = () => {
|
|
190
|
+
measurePlacement()
|
|
191
|
+
frame = requestAnimationFrame(track)
|
|
192
|
+
}
|
|
193
|
+
track()
|
|
194
|
+
|
|
195
|
+
return () => cancelAnimationFrame(frame)
|
|
196
|
+
}, [isOpen, measurePlacement])
|
|
186
197
|
|
|
187
198
|
React.useEffect(() => {
|
|
188
199
|
if (!isControlled || isFocused) return
|
|
@@ -251,7 +262,6 @@ export const SelectField = React.forwardRef<View, SelectFieldProps>(
|
|
|
251
262
|
}
|
|
252
263
|
|
|
253
264
|
const fieldState: InputState = error ? "error" : state
|
|
254
|
-
const isOpen = isFocused && !disabled
|
|
255
265
|
const clearAction =
|
|
256
266
|
!disabled && inputText.length > 0 ? (
|
|
257
267
|
<Pressable
|