@butternutbox/pawprint-native 1.6.3 → 2.0.1
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 +61 -0
- package/COMPONENT_GUIDELINES.md +57 -1
- package/dist/index.cjs +1459 -1380
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +81 -117
- package/dist/index.d.ts +81 -117
- package/dist/index.js +1206 -1125
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/__mocks__/react-native.tsx +33 -5
- package/src/__mocks__/rn-primitives/portal.tsx +71 -0
- package/src/components/atoms/Input/InputField.tsx +12 -2
- package/src/components/molecules/Drawer/Drawer.test.tsx +54 -2
- package/src/components/molecules/Drawer/DrawerBody.tsx +2 -17
- package/src/components/molecules/Drawer/DrawerContent.tsx +49 -2
- package/src/components/molecules/Drawer/DrawerOverlay.tsx +28 -16
- package/src/components/molecules/SelectField/AsyncSearch.test.tsx +373 -0
- package/src/components/molecules/SelectField/SelectDrawerField.stories.tsx +192 -0
- package/src/components/molecules/SelectField/SelectDrawerField.test.tsx +210 -0
- package/src/components/molecules/SelectField/SelectDrawerField.tsx +245 -0
- package/src/components/molecules/SelectField/SelectField.stories.tsx +221 -421
- package/src/components/molecules/SelectField/SelectField.test.tsx +214 -662
- package/src/components/molecules/SelectField/SelectField.tsx +269 -539
- package/src/components/molecules/SelectField/SelectFieldDropdown.tsx +90 -0
- package/src/components/molecules/SelectField/SelectFieldList.tsx +88 -0
- package/src/components/molecules/SelectField/SelectFieldOption.tsx +90 -0
- package/src/components/molecules/SelectField/core/index.ts +8 -0
- package/src/components/molecules/SelectField/core/listHeight.test.ts +22 -0
- package/src/components/molecules/SelectField/core/listHeight.ts +8 -0
- package/src/components/molecules/SelectField/core/options.ts +35 -0
- package/src/components/molecules/SelectField/core/useSelectOptions.ts +116 -0
- package/src/components/molecules/SelectField/index.ts +12 -9
- package/src/index.ts +0 -1
- package/src/test-utils.tsx +5 -1
- package/vitest.config.ts +7 -0
- package/src/components/molecules/SelectField/SelectFieldContent.tsx +0 -81
- package/src/components/molecules/SelectField/SelectFieldItem.tsx +0 -133
- package/src/components/molecules/SelectField/SelectFieldTrigger.tsx +0 -188
- package/src/components/molecules/SelectField/SelectFieldValue.tsx +0 -33
- package/src/components/molecules/SelectField/hooks/index.ts +0 -2
- package/src/components/molecules/SelectField/hooks/useSelectField.ts +0 -84
- package/src/hooks/index.ts +0 -1
- package/src/hooks/useDrawerKeyboardAvoidance.ts +0 -29
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@butternutbox/pawprint-native",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "ButternutBox Pawprint Design System - React Native Components",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"test": "vitest run"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@butternutbox/pawprint-fonts": "^2.0.
|
|
26
|
-
"@butternutbox/pawprint-tokens": "^0.6.
|
|
25
|
+
"@butternutbox/pawprint-fonts": "^2.0.2",
|
|
26
|
+
"@butternutbox/pawprint-tokens": "^0.6.1",
|
|
27
27
|
"@emotion/native": "^11.11.0",
|
|
28
28
|
"@emotion/react": "^11.14.0",
|
|
29
29
|
"@rn-primitives/avatar": "^1.4.0",
|
|
@@ -28,6 +28,15 @@ const attachMeasure =
|
|
|
28
28
|
MEASURED_TRIGGER.pageX,
|
|
29
29
|
MEASURED_TRIGGER.pageY
|
|
30
30
|
)
|
|
31
|
+
;(node as unknown as { measureInWindow: unknown }).measureInWindow = (
|
|
32
|
+
callback: (x: number, y: number, width: number, height: number) => void
|
|
33
|
+
) =>
|
|
34
|
+
callback(
|
|
35
|
+
MEASURED_TRIGGER.pageX,
|
|
36
|
+
MEASURED_TRIGGER.pageY,
|
|
37
|
+
MEASURED_TRIGGER.width,
|
|
38
|
+
MEASURED_TRIGGER.height
|
|
39
|
+
)
|
|
31
40
|
}
|
|
32
41
|
if (typeof ref === "function") ref(node)
|
|
33
42
|
else if (ref) (ref as React.RefObject<HTMLDivElement | null>).current = node
|
|
@@ -162,11 +171,13 @@ export const TextInput = React.forwardRef<HTMLInputElement, any>(
|
|
|
162
171
|
onChangeText,
|
|
163
172
|
onChange,
|
|
164
173
|
onBlur,
|
|
174
|
+
onFocus,
|
|
165
175
|
value,
|
|
166
176
|
defaultValue,
|
|
167
177
|
placeholder,
|
|
168
178
|
editable,
|
|
169
179
|
keyboardType: _keyboardType,
|
|
180
|
+
accessibilityRole,
|
|
170
181
|
accessibilityState,
|
|
171
182
|
..._rest
|
|
172
183
|
},
|
|
@@ -180,7 +191,10 @@ export const TextInput = React.forwardRef<HTMLInputElement, any>(
|
|
|
180
191
|
defaultValue={defaultValue}
|
|
181
192
|
placeholder={placeholder}
|
|
182
193
|
disabled={editable === false}
|
|
194
|
+
role={accessibilityRole}
|
|
183
195
|
aria-disabled={accessibilityState?.disabled}
|
|
196
|
+
aria-expanded={accessibilityState?.expanded}
|
|
197
|
+
onFocus={onFocus}
|
|
184
198
|
onBlur={onBlur}
|
|
185
199
|
onChange={(e) => {
|
|
186
200
|
onChangeText?.(e.target.value)
|
|
@@ -355,9 +369,16 @@ export const PanResponder = {
|
|
|
355
369
|
}
|
|
356
370
|
|
|
357
371
|
// ─── Easing ──────────────────────────────────────────────────────────────
|
|
372
|
+
type EasingFn = (t: number) => number
|
|
373
|
+
|
|
358
374
|
export const Easing = {
|
|
359
375
|
linear: (t: number) => t,
|
|
360
376
|
ease: (t: number) => t,
|
|
377
|
+
quad: (t: number) => t * t,
|
|
378
|
+
cubic: (t: number) => t * t * t,
|
|
379
|
+
in: (fn: EasingFn = (t) => t) => fn,
|
|
380
|
+
out: (fn: EasingFn = (t) => t) => fn,
|
|
381
|
+
inOut: (fn: EasingFn = (t) => t) => fn,
|
|
361
382
|
bezier: (_x1: number, _y1: number, _x2: number, _y2: number) => (t: number) =>
|
|
362
383
|
t
|
|
363
384
|
}
|
|
@@ -368,18 +389,25 @@ export const Platform = {
|
|
|
368
389
|
select: (obj: any) => obj.ios ?? obj.default
|
|
369
390
|
}
|
|
370
391
|
|
|
371
|
-
const keyboardHandlers = new Map<string, Set<() => void>>()
|
|
392
|
+
const keyboardHandlers = new Map<string, Set<(event?: unknown) => void>>()
|
|
393
|
+
|
|
394
|
+
const DEFAULT_KEYBOARD_EVENT = {
|
|
395
|
+
endCoordinates: { height: 336, screenX: 0, screenY: 476, width: 375 }
|
|
396
|
+
}
|
|
372
397
|
|
|
373
398
|
export const Keyboard = {
|
|
374
|
-
addListener: (event: string, handler: () => void) => {
|
|
399
|
+
addListener: (event: string, handler: (event?: unknown) => void) => {
|
|
375
400
|
const handlers = keyboardHandlers.get(event) ?? new Set()
|
|
376
401
|
handlers.add(handler)
|
|
377
402
|
keyboardHandlers.set(event, handlers)
|
|
378
403
|
return { remove: () => handlers.delete(handler) }
|
|
379
404
|
},
|
|
380
|
-
dismiss: () => {
|
|
381
|
-
|
|
382
|
-
|
|
405
|
+
dismiss: () => {
|
|
406
|
+
const active = document.activeElement
|
|
407
|
+
if (active instanceof HTMLElement) active.blur()
|
|
408
|
+
},
|
|
409
|
+
emit: (event: string, payload: unknown = DEFAULT_KEYBOARD_EVENT) => {
|
|
410
|
+
keyboardHandlers.get(event)?.forEach((handler) => handler(payload))
|
|
383
411
|
}
|
|
384
412
|
}
|
|
385
413
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
|
|
3
|
+
type PortalMap = Map<string, Map<string, React.ReactNode>>
|
|
4
|
+
|
|
5
|
+
const DEFAULT_HOST = "INTERNAL_PRIMITIVE_DEFAULT_HOST_NAME"
|
|
6
|
+
|
|
7
|
+
const portals: PortalMap = new Map([[DEFAULT_HOST, new Map()]])
|
|
8
|
+
const listeners = new Set<() => void>()
|
|
9
|
+
|
|
10
|
+
const notify = () => listeners.forEach((listener) => listener())
|
|
11
|
+
|
|
12
|
+
const subscribe = (listener: () => void) => {
|
|
13
|
+
listeners.add(listener)
|
|
14
|
+
return () => {
|
|
15
|
+
listeners.delete(listener)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let version = 0
|
|
20
|
+
const getSnapshot = () => version
|
|
21
|
+
|
|
22
|
+
export const PortalHost = ({ name = DEFAULT_HOST }: { name?: string }) => {
|
|
23
|
+
React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
|
|
24
|
+
const host = portals.get(name)
|
|
25
|
+
if (!host || host.size === 0) return null
|
|
26
|
+
return (
|
|
27
|
+
<>
|
|
28
|
+
{Array.from(host.entries()).map(([key, node]) => (
|
|
29
|
+
<React.Fragment key={key}>{node}</React.Fragment>
|
|
30
|
+
))}
|
|
31
|
+
</>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
PortalHost.displayName = "PortalHost"
|
|
35
|
+
|
|
36
|
+
export const Portal = ({
|
|
37
|
+
name,
|
|
38
|
+
hostName = DEFAULT_HOST,
|
|
39
|
+
children
|
|
40
|
+
}: {
|
|
41
|
+
name: string
|
|
42
|
+
hostName?: string
|
|
43
|
+
children: React.ReactNode
|
|
44
|
+
}) => {
|
|
45
|
+
React.useEffect(() => {
|
|
46
|
+
const host = portals.get(hostName) ?? new Map()
|
|
47
|
+
host.set(name, children)
|
|
48
|
+
portals.set(hostName, host)
|
|
49
|
+
version += 1
|
|
50
|
+
notify()
|
|
51
|
+
}, [hostName, name, children])
|
|
52
|
+
|
|
53
|
+
React.useEffect(
|
|
54
|
+
() => () => {
|
|
55
|
+
portals.get(hostName)?.delete(name)
|
|
56
|
+
version += 1
|
|
57
|
+
notify()
|
|
58
|
+
},
|
|
59
|
+
[hostName, name]
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
Portal.displayName = "Portal"
|
|
65
|
+
|
|
66
|
+
export const useModalPortalRoot = () => ({
|
|
67
|
+
ref: { current: null },
|
|
68
|
+
sideOffset: 0,
|
|
69
|
+
onLayout: () => {},
|
|
70
|
+
style: { flex: 1 }
|
|
71
|
+
})
|
|
@@ -27,6 +27,7 @@ type InputFieldOwnProps = {
|
|
|
27
27
|
containerWidth?: number
|
|
28
28
|
containerHeight?: number
|
|
29
29
|
hasInlineHelp?: boolean
|
|
30
|
+
onPress?: () => void
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export type InputFieldProps = InputFieldOwnProps &
|
|
@@ -131,6 +132,7 @@ export const InputField = React.forwardRef<TextInput, InputFieldProps>(
|
|
|
131
132
|
editable,
|
|
132
133
|
onFocus,
|
|
133
134
|
onBlur,
|
|
135
|
+
onPress,
|
|
134
136
|
...rest
|
|
135
137
|
},
|
|
136
138
|
ref
|
|
@@ -154,7 +156,11 @@ export const InputField = React.forwardRef<TextInput, InputFieldProps>(
|
|
|
154
156
|
},
|
|
155
157
|
[ref]
|
|
156
158
|
)
|
|
157
|
-
|
|
159
|
+
// `editable={false}` on its own means a disabled field. Paired with
|
|
160
|
+
// `onPress` it means a field that displays a value and opens something else
|
|
161
|
+
// (e.g. SelectDrawerField), which is interactive and must not render in the
|
|
162
|
+
// disabled palette.
|
|
163
|
+
const isDisabled = editable === false && onPress === undefined
|
|
158
164
|
const hasState = state === "error" || state === "success"
|
|
159
165
|
|
|
160
166
|
const baseBorderWidth = parseTokenValue(borderWidth.field.default)
|
|
@@ -242,7 +248,7 @@ export const InputField = React.forwardRef<TextInput, InputFieldProps>(
|
|
|
242
248
|
}
|
|
243
249
|
|
|
244
250
|
return (
|
|
245
|
-
<Pressable onPress={handleWrapperPress}>
|
|
251
|
+
<Pressable onPress={onPress ?? handleWrapperPress}>
|
|
246
252
|
<StyledInputWrapper
|
|
247
253
|
isDisabled={isDisabled}
|
|
248
254
|
containerWidth={containerWidth}
|
|
@@ -275,6 +281,10 @@ export const InputField = React.forwardRef<TextInput, InputFieldProps>(
|
|
|
275
281
|
style={style}
|
|
276
282
|
placeholderTextColor={colour.field.text.placeholder}
|
|
277
283
|
editable={editable}
|
|
284
|
+
// A non-editable TextInput still captures touches in its own area
|
|
285
|
+
// and does nothing with them, so without this only taps on the
|
|
286
|
+
// surrounding padding would reach the wrapper's `onPress`.
|
|
287
|
+
pointerEvents={onPress ? "none" : undefined}
|
|
278
288
|
onFocus={handleFocus}
|
|
279
289
|
onBlur={handleBlur}
|
|
280
290
|
hasInlineHelp={hasInlineHelp}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import React from "react"
|
|
2
|
-
import { screen, fireEvent } from "@testing-library/react"
|
|
3
|
-
import { describe, it, expect, vi } from "vitest"
|
|
2
|
+
import { screen, fireEvent, act } from "@testing-library/react"
|
|
3
|
+
import { describe, it, expect, vi, afterEach } from "vitest"
|
|
4
|
+
import { Keyboard } from "react-native"
|
|
4
5
|
import { renderWithTheme } from "../../../test-utils"
|
|
6
|
+
import { Typography } from "../../atoms/Typography"
|
|
5
7
|
import { Drawer } from "./Drawer"
|
|
6
8
|
|
|
7
9
|
const BasicDrawer = ({
|
|
@@ -40,6 +42,56 @@ const BasicDrawer = ({
|
|
|
40
42
|
</Drawer.Root>
|
|
41
43
|
)
|
|
42
44
|
|
|
45
|
+
describe("Drawer keyboard lift", () => {
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
Keyboard.emit("keyboardDidHide", {
|
|
48
|
+
endCoordinates: { height: 0 },
|
|
49
|
+
duration: 0
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it("renders its content while the keyboard is closed", () => {
|
|
54
|
+
renderWithTheme(
|
|
55
|
+
<Drawer.Root defaultOpen>
|
|
56
|
+
<Drawer.Portal>
|
|
57
|
+
<Drawer.Overlay />
|
|
58
|
+
<Drawer.Content>
|
|
59
|
+
<Drawer.Body>
|
|
60
|
+
<Typography>Row one</Typography>
|
|
61
|
+
</Drawer.Body>
|
|
62
|
+
</Drawer.Content>
|
|
63
|
+
</Drawer.Portal>
|
|
64
|
+
</Drawer.Root>
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
expect(screen.getByText("Row one")).toBeInTheDocument()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it("keeps its content when the keyboard opens", () => {
|
|
71
|
+
renderWithTheme(
|
|
72
|
+
<Drawer.Root defaultOpen>
|
|
73
|
+
<Drawer.Portal>
|
|
74
|
+
<Drawer.Overlay />
|
|
75
|
+
<Drawer.Content>
|
|
76
|
+
<Drawer.Body>
|
|
77
|
+
<Typography>Row one</Typography>
|
|
78
|
+
</Drawer.Body>
|
|
79
|
+
</Drawer.Content>
|
|
80
|
+
</Drawer.Portal>
|
|
81
|
+
</Drawer.Root>
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
act(() => {
|
|
85
|
+
Keyboard.emit("keyboardWillShow", {
|
|
86
|
+
endCoordinates: { height: 336 },
|
|
87
|
+
duration: 250
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
expect(screen.getByText("Row one")).toBeInTheDocument()
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
43
95
|
describe("Drawer", () => {
|
|
44
96
|
describe("rendering", () => {
|
|
45
97
|
it("renders the trigger", () => {
|
|
@@ -6,11 +6,8 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"
|
|
|
6
6
|
import { useDrawerHeaderContext } from "./DrawerHeaderContext"
|
|
7
7
|
import { useDrawerFooterContext } from "./DrawerFooterContext"
|
|
8
8
|
import { useDrawerBodyMax } from "./DrawerBodyMaxContext"
|
|
9
|
-
import { useDrawerKeyboardAvoidance } from "../../../hooks/useDrawerKeyboardAvoidance"
|
|
10
9
|
|
|
11
|
-
export type DrawerBodyProps = ScrollViewProps
|
|
12
|
-
ignoreKeyboardAvoidance?: boolean
|
|
13
|
-
}
|
|
10
|
+
export type DrawerBodyProps = ScrollViewProps
|
|
14
11
|
|
|
15
12
|
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
16
13
|
|
|
@@ -43,15 +40,7 @@ const StyledScrollView = styled(ScrollView)<{
|
|
|
43
40
|
* chrome-aware `maxHeight`, scrolling beyond that.
|
|
44
41
|
*/
|
|
45
42
|
export const DrawerBody = React.forwardRef<ScrollView, DrawerBodyProps>(
|
|
46
|
-
(
|
|
47
|
-
{
|
|
48
|
-
children,
|
|
49
|
-
contentContainerStyle,
|
|
50
|
-
ignoreKeyboardAvoidance = false,
|
|
51
|
-
...props
|
|
52
|
-
},
|
|
53
|
-
ref
|
|
54
|
-
) => {
|
|
43
|
+
({ children, contentContainerStyle, ...props }, ref) => {
|
|
55
44
|
const theme = useTheme()
|
|
56
45
|
const { spacing } = theme.tokens.components.drawer
|
|
57
46
|
const { buttons } = theme.tokens.components
|
|
@@ -61,9 +50,6 @@ export const DrawerBody = React.forwardRef<ScrollView, DrawerBodyProps>(
|
|
|
61
50
|
const providedMaxHeight = useDrawerBodyMax()
|
|
62
51
|
const { height: windowHeight } = useWindowDimensions()
|
|
63
52
|
const insets = useSafeAreaInsets()
|
|
64
|
-
const { contentContainerStyle: keyboardStyle } =
|
|
65
|
-
useDrawerKeyboardAvoidance()
|
|
66
|
-
const effectiveKeyboardStyle = ignoreKeyboardAvoidance ? {} : keyboardStyle
|
|
67
53
|
|
|
68
54
|
const gap = parseTokenValue(content.slot.gap)
|
|
69
55
|
const horizontalPadding = parseTokenValue(content.slot.horizontalPadding)
|
|
@@ -107,7 +93,6 @@ export const DrawerBody = React.forwardRef<ScrollView, DrawerBodyProps>(
|
|
|
107
93
|
paddingTop: topPadding,
|
|
108
94
|
paddingBottom: 0
|
|
109
95
|
},
|
|
110
|
-
effectiveKeyboardStyle,
|
|
111
96
|
contentContainerStyle
|
|
112
97
|
]}
|
|
113
98
|
{...props}
|
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
PanResponder,
|
|
6
6
|
StyleSheet,
|
|
7
7
|
View,
|
|
8
|
+
Keyboard,
|
|
9
|
+
Platform,
|
|
8
10
|
useWindowDimensions,
|
|
9
11
|
type ViewProps
|
|
10
12
|
} from "react-native"
|
|
@@ -22,6 +24,7 @@ import {
|
|
|
22
24
|
|
|
23
25
|
export type DrawerContentProps = Omit<ViewProps, "style">
|
|
24
26
|
|
|
27
|
+
const KEYBOARD_DURATION = 250
|
|
25
28
|
const OPEN_DURATION = 320
|
|
26
29
|
const CLOSE_DURATION = 220
|
|
27
30
|
const DISMISS_THRESHOLD = 80
|
|
@@ -108,7 +111,9 @@ export const DrawerContent = React.forwardRef<View, DrawerContentProps>(
|
|
|
108
111
|
const { isOpen, closeDrawer, onExitComplete } = useDrawerContext()
|
|
109
112
|
|
|
110
113
|
const { height: windowHeight } = useWindowDimensions()
|
|
111
|
-
const
|
|
114
|
+
const [keyboardHeight, setKeyboardHeight] = useState(0)
|
|
115
|
+
const keyboardSpacer = useRef(new Animated.Value(0)).current
|
|
116
|
+
const maxHeight = (windowHeight - keyboardHeight) * 0.9
|
|
112
117
|
|
|
113
118
|
// Constant parts of the chrome that aren't a measured section: the panel's
|
|
114
119
|
// top border, and (when there's no footer) the body's own bottom padding.
|
|
@@ -134,6 +139,7 @@ export const DrawerContent = React.forwardRef<View, DrawerContentProps>(
|
|
|
134
139
|
// ScrollView's `maxHeight`, so tall content scrolls within the cap and short
|
|
135
140
|
// content lets the panel wrap tight. Null until the chrome is measured, in
|
|
136
141
|
// which case DrawerBody falls back to its own window-based heuristic.
|
|
142
|
+
const applyBodyMaxRef = useRef<(() => void) | null>(null)
|
|
137
143
|
const [bodyMax, setBodyMax] = useState<number | null>(null)
|
|
138
144
|
const bodyMaxRef = useRef<number | null>(null)
|
|
139
145
|
|
|
@@ -153,6 +159,39 @@ export const DrawerContent = React.forwardRef<View, DrawerContentProps>(
|
|
|
153
159
|
return () => translateY.removeListener(id)
|
|
154
160
|
}, [translateY])
|
|
155
161
|
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
const showEvent =
|
|
164
|
+
Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow"
|
|
165
|
+
const hideEvent =
|
|
166
|
+
Platform.OS === "ios" ? "keyboardWillHide" : "keyboardDidHide"
|
|
167
|
+
|
|
168
|
+
// Height, not transform: translating the panel would lift it off the
|
|
169
|
+
// bottom of the screen and show the backdrop beneath it. Height can't be
|
|
170
|
+
// native-driven, but it is one view for the keyboard's own duration.
|
|
171
|
+
const lift = (toValue: number, duration: number | undefined) => {
|
|
172
|
+
Animated.timing(keyboardSpacer, {
|
|
173
|
+
toValue,
|
|
174
|
+
duration: duration && duration > 0 ? duration : KEYBOARD_DURATION,
|
|
175
|
+
easing: Easing.out(Easing.ease),
|
|
176
|
+
useNativeDriver: false
|
|
177
|
+
}).start()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const shown = Keyboard.addListener(showEvent, (event) => {
|
|
181
|
+
setKeyboardHeight(event.endCoordinates.height)
|
|
182
|
+
lift(event.endCoordinates.height, event.duration)
|
|
183
|
+
})
|
|
184
|
+
const hidden = Keyboard.addListener(hideEvent, (event) => {
|
|
185
|
+
setKeyboardHeight(0)
|
|
186
|
+
lift(0, event.duration)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
return () => {
|
|
190
|
+
shown.remove()
|
|
191
|
+
hidden.remove()
|
|
192
|
+
}
|
|
193
|
+
}, [keyboardSpacer])
|
|
194
|
+
|
|
156
195
|
// Clamp the cached panel height when the window shrinks (e.g. orientation
|
|
157
196
|
// change) so the entry animation never starts from below the screen. This
|
|
158
197
|
// is animation-only bookkeeping — the panel itself sizes from its content.
|
|
@@ -162,6 +201,10 @@ export const DrawerContent = React.forwardRef<View, DrawerContentProps>(
|
|
|
162
201
|
}
|
|
163
202
|
}, [maxHeight])
|
|
164
203
|
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
applyBodyMaxRef.current?.()
|
|
206
|
+
}, [maxHeight])
|
|
207
|
+
|
|
165
208
|
const runEntry = useCallback(() => {
|
|
166
209
|
if (panelHeightRef.current === 0) return
|
|
167
210
|
translateY.setValue(panelHeightRef.current)
|
|
@@ -236,6 +279,8 @@ export const DrawerContent = React.forwardRef<View, DrawerContentProps>(
|
|
|
236
279
|
}
|
|
237
280
|
}, [measureChrome, maxHeight])
|
|
238
281
|
|
|
282
|
+
applyBodyMaxRef.current = applyBodyMax
|
|
283
|
+
|
|
239
284
|
const setHeaderHeight = useCallback(
|
|
240
285
|
(height: number) => {
|
|
241
286
|
if (height <= 0 || Math.abs(height - headerHeightRef.current) < 1)
|
|
@@ -370,6 +415,7 @@ export const DrawerContent = React.forwardRef<View, DrawerContentProps>(
|
|
|
370
415
|
<DrawerMeasureContext.Provider value={measureContextValue}>
|
|
371
416
|
<DrawerDragContext.Provider value={dragContextValue}>
|
|
372
417
|
<Animated.View
|
|
418
|
+
pointerEvents="box-none"
|
|
373
419
|
style={[
|
|
374
420
|
styles.animatedWrapper,
|
|
375
421
|
{ transform: [{ translateY }] }
|
|
@@ -381,7 +427,7 @@ export const DrawerContent = React.forwardRef<View, DrawerContentProps>(
|
|
|
381
427
|
panelBorderRadius={parseTokenValue(drawer.borderRadius.top)}
|
|
382
428
|
panelBgColor={colour.background.container.default}
|
|
383
429
|
panelMaxWidth={parseTokenValue(drawer.size.maxWidth)}
|
|
384
|
-
panelMaxHeight={maxHeight}
|
|
430
|
+
panelMaxHeight={maxHeight + keyboardHeight}
|
|
385
431
|
panelBorderWidth={parseTokenValue(dimensions.borderWidth.sm)}
|
|
386
432
|
panelBorderColor={colour.border.default}
|
|
387
433
|
panelShadowColor={modal.shadow.color}
|
|
@@ -395,6 +441,7 @@ export const DrawerContent = React.forwardRef<View, DrawerContentProps>(
|
|
|
395
441
|
<DrawerBodyMaxContext.Provider value={bodyMax}>
|
|
396
442
|
{children}
|
|
397
443
|
</DrawerBodyMaxContext.Provider>
|
|
444
|
+
<Animated.View style={{ height: keyboardSpacer }} />
|
|
398
445
|
</StyledPanel>
|
|
399
446
|
</Animated.View>
|
|
400
447
|
</DrawerDragContext.Provider>
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import React, { useEffect, useRef } from "react"
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
Animated,
|
|
4
|
+
Pressable,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
useWindowDimensions
|
|
7
|
+
} from "react-native"
|
|
3
8
|
import { useTheme } from "@emotion/react"
|
|
4
9
|
import { useDrawerContext } from "./DrawerContext"
|
|
5
10
|
|
|
@@ -19,6 +24,7 @@ export const DrawerOverlay = React.forwardRef<
|
|
|
19
24
|
DrawerOverlayProps
|
|
20
25
|
>(({ accessible = false }, _ref) => {
|
|
21
26
|
const theme = useTheme()
|
|
27
|
+
const { width: screenWidth, height: screenHeight } = useWindowDimensions()
|
|
22
28
|
const { isOpen, closeDrawer, backdrop } = useDrawerContext()
|
|
23
29
|
const opacity = useRef(new Animated.Value(0)).current
|
|
24
30
|
|
|
@@ -34,22 +40,28 @@ export const DrawerOverlay = React.forwardRef<
|
|
|
34
40
|
if (!backdrop) return null
|
|
35
41
|
|
|
36
42
|
return (
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
<Animated.View
|
|
44
|
+
pointerEvents="box-none"
|
|
45
|
+
style={[
|
|
46
|
+
StyleSheet.absoluteFillObject,
|
|
47
|
+
{ width: screenWidth, height: screenHeight, opacity }
|
|
48
|
+
]}
|
|
42
49
|
>
|
|
43
|
-
<
|
|
44
|
-
style={
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
<Pressable
|
|
51
|
+
style={{ width: screenWidth, height: screenHeight }}
|
|
52
|
+
onPress={closeDrawer}
|
|
53
|
+
accessible={accessible}
|
|
54
|
+
accessibilityLabel="Close drawer"
|
|
55
|
+
>
|
|
56
|
+
<Animated.View
|
|
57
|
+
style={{
|
|
58
|
+
width: screenWidth,
|
|
59
|
+
height: screenHeight,
|
|
60
|
+
backgroundColor: theme.tokens.components.modal.overlay.colour
|
|
61
|
+
}}
|
|
62
|
+
/>
|
|
63
|
+
</Pressable>
|
|
64
|
+
</Animated.View>
|
|
53
65
|
)
|
|
54
66
|
})
|
|
55
67
|
|