@butternutbox/pawprint-native 0.0.1 → 0.1.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 +15 -15
- package/CHANGELOG.md +16 -0
- package/COMPONENT_GUIDELINES.md +111 -4
- package/dist/index.cjs +12370 -1455
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1110 -11
- package/dist/index.d.ts +1110 -11
- package/dist/index.js +12324 -1455
- package/dist/index.js.map +1 -1
- package/package.json +28 -9
- package/src/__mocks__/asset-stub.ts +1 -0
- package/src/__mocks__/emotion-native.tsx +18 -0
- package/src/__mocks__/react-native-gesture-handler.tsx +41 -0
- package/src/__mocks__/react-native-reanimated.tsx +79 -0
- package/src/__mocks__/react-native-safe-area-context.tsx +6 -0
- package/src/__mocks__/react-native-svg.tsx +27 -0
- package/src/__mocks__/react-native-worklets.tsx +11 -0
- package/src/__mocks__/react-native.tsx +338 -0
- package/src/__mocks__/rn-primitives/avatar.tsx +24 -0
- package/src/__mocks__/rn-primitives/checkbox.tsx +19 -0
- package/src/__mocks__/rn-primitives/select.tsx +116 -0
- package/src/__mocks__/rn-primitives/slider.tsx +40 -0
- package/src/__mocks__/rn-primitives/slot.tsx +30 -0
- package/src/__mocks__/rn-primitives/switch.tsx +24 -0
- package/src/__mocks__/rn-primitives/toggle.tsx +16 -0
- package/src/components/atoms/Avatar/Avatar.stories.tsx +57 -49
- package/src/components/atoms/Avatar/Avatar.test.tsx +269 -0
- package/src/components/atoms/Avatar/Avatar.tsx +68 -22
- package/src/components/atoms/Avatar/index.ts +1 -6
- package/src/components/atoms/Badge/Badge.stories.tsx +5 -29
- package/src/components/atoms/Badge/Badge.test.tsx +90 -0
- package/src/components/atoms/Button/Button.test.tsx +123 -0
- package/src/components/atoms/Button/Button.tsx +1 -1
- package/src/components/atoms/CarouselControls/CarouselControls.stories.tsx +217 -0
- package/src/components/atoms/CarouselControls/CarouselControls.tsx +127 -0
- package/src/components/atoms/CarouselControls/index.ts +2 -0
- package/src/components/atoms/Hint/Hint.test.tsx +36 -0
- package/src/components/atoms/Icon/Icon.test.tsx +98 -0
- package/src/components/atoms/Icon/Icon.tsx +5 -1
- package/src/components/atoms/IconButton/IconButton.test.tsx +101 -0
- package/src/components/atoms/Illustration/Illustration.test.tsx +55 -0
- package/src/components/atoms/Input/Input.stories.tsx +129 -86
- package/src/components/atoms/Input/Input.test.tsx +306 -0
- package/src/components/atoms/Input/Input.tsx +9 -1
- package/src/components/atoms/Input/InputField.tsx +226 -74
- package/src/components/atoms/Link/Link.test.tsx +89 -0
- package/src/components/atoms/Logo/Logo.registry.ts +30 -5
- package/src/components/atoms/Logo/Logo.stories.tsx +108 -0
- package/src/components/atoms/Logo/Logo.test.tsx +56 -0
- package/src/components/atoms/Logo/assets/BCorp.tsx +113 -0
- package/src/components/atoms/Logo/assets/ButternutFavicon.tsx +33 -0
- package/src/components/atoms/Logo/assets/ButternutPrimary.tsx +294 -0
- package/src/components/atoms/Logo/assets/ButternutTabbedBottom.tsx +294 -0
- package/src/components/atoms/Logo/assets/ButternutTabbedTop.tsx +294 -0
- package/src/components/atoms/Logo/assets/ButternutWordmark.tsx +274 -0
- package/src/components/atoms/Logo/assets/PsiBufetFavicon.tsx +45 -0
- package/src/components/atoms/Logo/assets/PsiBufetPrimary.tsx +218 -0
- package/src/components/atoms/Logo/assets/PsiBufetTabbedBottom.tsx +218 -0
- package/src/components/atoms/Logo/assets/PsiBufetTabbedTop.tsx +218 -0
- package/src/components/atoms/Logo/assets/PsiBufetWordmark.tsx +195 -0
- package/src/components/atoms/Logo/assets/index.ts +11 -0
- package/src/components/atoms/NumberInput/NumberInput.stories.tsx +183 -0
- package/src/components/atoms/NumberInput/NumberInput.test.tsx +261 -0
- package/src/components/atoms/NumberInput/NumberInput.tsx +129 -0
- package/src/components/atoms/NumberInput/NumberInputField.tsx +77 -0
- package/src/components/atoms/NumberInput/index.ts +4 -0
- package/src/components/atoms/Spinner/Spinner.test.tsx +46 -0
- package/src/components/atoms/Spinner/Spinner.tsx +14 -5
- package/src/components/atoms/Switch/Switch.test.tsx +92 -0
- package/src/components/atoms/Switch/Switch.tsx +16 -13
- package/src/components/atoms/Tag/Tag.test.tsx +70 -0
- package/src/components/atoms/TextArea/TextArea.stories.tsx +303 -0
- package/src/components/atoms/TextArea/TextArea.test.tsx +416 -0
- package/src/components/atoms/TextArea/TextArea.tsx +171 -0
- package/src/components/atoms/TextArea/TextAreaField.tsx +304 -0
- package/src/components/atoms/TextArea/TextAreaLabel.tsx +103 -0
- package/src/components/atoms/TextArea/index.ts +6 -0
- package/src/components/atoms/Typography/Typography.test.tsx +94 -0
- package/src/components/atoms/index.ts +3 -0
- package/src/components/molecules/Accordion/Accordion.stories.tsx +177 -0
- package/src/components/molecules/Accordion/Accordion.test.tsx +185 -0
- package/src/components/molecules/Accordion/Accordion.tsx +284 -0
- package/src/components/molecules/Accordion/index.ts +6 -0
- package/src/components/molecules/Animated/Animated.stories.tsx +254 -0
- package/src/components/molecules/Animated/Animated.tsx +283 -0
- package/src/components/molecules/Animated/index.ts +10 -0
- package/src/components/molecules/ButtonDock/ButtonDock.test.tsx +83 -0
- package/src/components/molecules/ButtonGroup/ButtonGroup.stories.tsx +8 -14
- package/src/components/molecules/ButtonGroup/ButtonGroup.test.tsx +73 -0
- package/src/components/molecules/ButtonGroup/ButtonGroup.tsx +25 -3
- package/src/components/molecules/Checkbox/Checkbox.stories.tsx +72 -0
- package/src/components/molecules/Checkbox/Checkbox.test.tsx +117 -0
- package/src/components/molecules/Checkbox/Checkbox.tsx +101 -95
- package/src/components/molecules/CopyField/CopyField.stories.tsx +313 -0
- package/src/components/molecules/CopyField/CopyField.test.tsx +431 -0
- package/src/components/molecules/CopyField/CopyField.tsx +156 -0
- package/src/components/molecules/CopyField/CopyFieldInput.tsx +127 -0
- package/src/components/molecules/CopyField/hooks/index.ts +1 -0
- package/src/components/molecules/CopyField/hooks/useCopyField.ts +25 -0
- package/src/components/molecules/CopyField/index.ts +4 -0
- package/src/components/molecules/DatePicker/DatePicker.stories.tsx +298 -0
- package/src/components/molecules/DatePicker/DatePicker.test.tsx +201 -0
- package/src/components/molecules/DatePicker/DatePicker.tsx +590 -0
- package/src/components/molecules/DatePicker/index.ts +2 -0
- package/src/components/molecules/Drawer/Drawer.stories.tsx +285 -0
- package/src/components/molecules/Drawer/Drawer.test.tsx +180 -0
- package/src/components/molecules/Drawer/Drawer.tsx +187 -0
- package/src/components/molecules/Drawer/DrawerBody.tsx +80 -0
- package/src/components/molecules/Drawer/DrawerClose.tsx +76 -0
- package/src/components/molecules/Drawer/DrawerContent.tsx +339 -0
- package/src/components/molecules/Drawer/DrawerContext.ts +19 -0
- package/src/components/molecules/Drawer/DrawerDescription.tsx +31 -0
- package/src/components/molecules/Drawer/DrawerDragContext.ts +11 -0
- package/src/components/molecules/Drawer/DrawerFooter.tsx +49 -0
- package/src/components/molecules/Drawer/DrawerFooterContext.ts +6 -0
- package/src/components/molecules/Drawer/DrawerGrabber.tsx +62 -0
- package/src/components/molecules/Drawer/DrawerHeader.tsx +244 -0
- package/src/components/molecules/Drawer/DrawerHeaderContext.ts +13 -0
- package/src/components/molecules/Drawer/DrawerOverlay.tsx +53 -0
- package/src/components/molecules/Drawer/DrawerTitle.tsx +32 -0
- package/src/components/molecules/Drawer/index.ts +12 -0
- package/src/components/molecules/FilterTab/FilterTab.stories.tsx +210 -0
- package/src/components/molecules/FilterTab/FilterTab.tsx +310 -0
- package/src/components/molecules/FilterTab/index.ts +2 -0
- package/src/components/molecules/MessageCard/MessageCard.stories.tsx +169 -0
- package/src/components/molecules/MessageCard/MessageCard.tsx +362 -0
- package/src/components/molecules/MessageCard/index.ts +10 -0
- package/src/components/molecules/Notification/Notification.stories.tsx +219 -0
- package/src/components/molecules/Notification/Notification.tsx +426 -0
- package/src/components/molecules/Notification/index.ts +2 -0
- package/src/components/molecules/NumberField/NumberField.stories.tsx +231 -0
- package/src/components/molecules/NumberField/NumberField.tsx +186 -0
- package/src/components/molecules/NumberField/NumberFieldInput.tsx +287 -0
- package/src/components/molecules/NumberField/index.ts +2 -0
- package/src/components/molecules/PasswordField/PasswordField.stories.tsx +362 -0
- package/src/components/molecules/PasswordField/PasswordField.test.tsx +369 -0
- package/src/components/molecules/PasswordField/PasswordField.tsx +194 -0
- package/src/components/molecules/PasswordField/PasswordFieldError.tsx +52 -0
- package/src/components/molecules/PasswordField/PasswordFieldInput.tsx +73 -0
- package/src/components/molecules/PasswordField/PasswordFieldRequirements.tsx +92 -0
- package/src/components/molecules/PasswordField/hooks/index.ts +2 -0
- package/src/components/molecules/PasswordField/hooks/usePasswordField.ts +113 -0
- package/src/components/molecules/PasswordField/index.ts +10 -0
- package/src/components/molecules/PictureSelector/PictureSelector.stories.tsx +243 -0
- package/src/components/molecules/PictureSelector/PictureSelector.tsx +313 -0
- package/src/components/molecules/PictureSelector/index.ts +5 -0
- package/src/components/molecules/Progress/Progress.stories.tsx +145 -0
- package/src/components/molecules/Progress/Progress.tsx +184 -0
- package/src/components/molecules/Progress/index.ts +2 -0
- package/src/components/molecules/Radio/Radio.test.tsx +104 -0
- package/src/components/molecules/Radio/Radio.tsx +1 -2
- package/src/components/molecules/SearchField/SearchField.stories.tsx +242 -0
- package/src/components/molecules/SearchField/SearchField.test.tsx +318 -0
- package/src/components/molecules/SearchField/SearchField.tsx +143 -0
- package/src/components/molecules/SearchField/SearchFieldInput.tsx +63 -0
- package/src/components/molecules/SearchField/hooks/index.ts +1 -0
- package/src/components/molecules/SearchField/hooks/useSearchField.ts +56 -0
- package/src/components/molecules/SearchField/index.ts +4 -0
- package/src/components/molecules/SegmentedControl/SegmentedControl.stories.tsx +31 -8
- package/src/components/molecules/SegmentedControl/SegmentedControl.test.tsx +141 -0
- package/src/components/molecules/SegmentedControl/SegmentedControl.tsx +237 -23
- package/src/components/molecules/SelectField/SelectField.stories.tsx +320 -0
- package/src/components/molecules/SelectField/SelectField.test.tsx +254 -0
- package/src/components/molecules/SelectField/SelectField.tsx +236 -0
- package/src/components/molecules/SelectField/SelectFieldContent.tsx +85 -0
- package/src/components/molecules/SelectField/SelectFieldItem.tsx +133 -0
- package/src/components/molecules/SelectField/SelectFieldTrigger.tsx +170 -0
- package/src/components/molecules/SelectField/SelectFieldValue.tsx +31 -0
- package/src/components/molecules/SelectField/hooks/index.ts +2 -0
- package/src/components/molecules/SelectField/hooks/useSelectField.ts +84 -0
- package/src/components/molecules/SelectField/index.ts +10 -0
- package/src/components/molecules/Slider/Slider.test.tsx +102 -0
- package/src/components/molecules/Slider/Slider.tsx +293 -180
- package/src/components/molecules/Tooltip/Tooltip.stories.tsx +168 -0
- package/src/components/molecules/Tooltip/Tooltip.tsx +326 -0
- package/src/components/molecules/Tooltip/index.ts +2 -0
- package/src/components/molecules/index.ts +15 -0
- package/src/test-utils.tsx +20 -0
- package/tsconfig.json +1 -1
- package/tsup.config.ts +16 -2
- package/vitest.config.ts +114 -0
- package/vitest.setup.ts +16 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { ScrollView, ScrollViewProps } from "react-native"
|
|
3
|
+
import styled from "@emotion/native"
|
|
4
|
+
import { useTheme } from "@emotion/react"
|
|
5
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context"
|
|
6
|
+
import { useDrawerHeaderContext } from "./DrawerHeaderContext"
|
|
7
|
+
import { useDrawerFooterContext } from "./DrawerFooterContext"
|
|
8
|
+
|
|
9
|
+
export type DrawerBodyProps = ScrollViewProps
|
|
10
|
+
|
|
11
|
+
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
12
|
+
|
|
13
|
+
const StyledScrollView = styled(ScrollView)<{
|
|
14
|
+
bodyPaddingHorizontal: number
|
|
15
|
+
bodyPaddingRight: number
|
|
16
|
+
bodyGap: number
|
|
17
|
+
bodyPaddingVertical: number
|
|
18
|
+
}>(
|
|
19
|
+
({
|
|
20
|
+
bodyPaddingHorizontal,
|
|
21
|
+
bodyPaddingRight,
|
|
22
|
+
bodyGap,
|
|
23
|
+
bodyPaddingVertical
|
|
24
|
+
}) => ({
|
|
25
|
+
flexGrow: 1,
|
|
26
|
+
flexShrink: 1,
|
|
27
|
+
paddingLeft: bodyPaddingHorizontal,
|
|
28
|
+
paddingRight: bodyPaddingRight,
|
|
29
|
+
gap: bodyGap,
|
|
30
|
+
paddingTop: bodyPaddingVertical
|
|
31
|
+
})
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Scrollable content area of the drawer. Grows to fill available space
|
|
36
|
+
* between the header and footer.
|
|
37
|
+
*/
|
|
38
|
+
export const DrawerBody = React.forwardRef<ScrollView, DrawerBodyProps>(
|
|
39
|
+
({ children, contentContainerStyle, ...props }, ref) => {
|
|
40
|
+
const theme = useTheme()
|
|
41
|
+
const { spacing } = theme.tokens.components.drawer
|
|
42
|
+
const { buttons } = theme.tokens.components
|
|
43
|
+
const { content } = spacing
|
|
44
|
+
const headerContext = useDrawerHeaderContext()
|
|
45
|
+
const hasFooter = useDrawerFooterContext()
|
|
46
|
+
const insets = useSafeAreaInsets()
|
|
47
|
+
|
|
48
|
+
const gap = parseTokenValue(content.slot.gap)
|
|
49
|
+
const verticalPadding = parseTokenValue(content.slot.verticalPadding)
|
|
50
|
+
const bottomPadding = verticalPadding + (hasFooter ? 0 : insets.bottom)
|
|
51
|
+
const horizontalPadding = parseTokenValue(content.slot.horizontalPadding)
|
|
52
|
+
|
|
53
|
+
// When there's no header the close button floats in the top-right corner.
|
|
54
|
+
// Reserve space on the right so body content doesn't slide under it.
|
|
55
|
+
const paddingRight =
|
|
56
|
+
headerContext === null
|
|
57
|
+
? parseTokenValue(spacing.close.right.md) +
|
|
58
|
+
parseTokenValue(buttons.size.sm.height)
|
|
59
|
+
: horizontalPadding
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<StyledScrollView
|
|
63
|
+
ref={ref}
|
|
64
|
+
bodyPaddingHorizontal={horizontalPadding}
|
|
65
|
+
bodyPaddingRight={paddingRight}
|
|
66
|
+
bodyGap={gap}
|
|
67
|
+
bodyPaddingVertical={verticalPadding}
|
|
68
|
+
contentContainerStyle={[
|
|
69
|
+
{ gap, paddingBottom: bottomPadding },
|
|
70
|
+
contentContainerStyle
|
|
71
|
+
]}
|
|
72
|
+
{...props}
|
|
73
|
+
>
|
|
74
|
+
{children}
|
|
75
|
+
</StyledScrollView>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
DrawerBody.displayName = "Drawer.Body"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { View, ViewProps, StyleSheet } from "react-native"
|
|
3
|
+
import { Close as CloseIcon } from "@butternutbox/pawprint-icons/core"
|
|
4
|
+
import { useTheme } from "@emotion/react"
|
|
5
|
+
import { IconButton } from "../../atoms/IconButton"
|
|
6
|
+
import { useDrawerContext } from "./DrawerContext"
|
|
7
|
+
import { useDrawerHeaderContext } from "./DrawerHeaderContext"
|
|
8
|
+
|
|
9
|
+
export type DrawerCloseProps = {
|
|
10
|
+
"aria-label"?: string
|
|
11
|
+
} & Omit<ViewProps, "children">
|
|
12
|
+
|
|
13
|
+
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Close button for the drawer. When placed inside `Drawer.Header` the header
|
|
17
|
+
* manages its position. When used outside a header it renders as a floating
|
|
18
|
+
* button in the top-right corner of the panel.
|
|
19
|
+
*
|
|
20
|
+
* Colour is `"primary"` for the `image` variant and `"secondary"` otherwise.
|
|
21
|
+
*/
|
|
22
|
+
export const DrawerClose = React.forwardRef<View, DrawerCloseProps>(
|
|
23
|
+
({ "aria-label": ariaLabel = "Close", ...props }, ref) => {
|
|
24
|
+
const theme = useTheme()
|
|
25
|
+
const { closeDrawer } = useDrawerContext()
|
|
26
|
+
const headerContext = useDrawerHeaderContext()
|
|
27
|
+
const { spacing } = theme.tokens.components.drawer
|
|
28
|
+
|
|
29
|
+
const defaultColour =
|
|
30
|
+
headerContext?.variant === "image" ? "primary" : "secondary"
|
|
31
|
+
|
|
32
|
+
const button = (
|
|
33
|
+
<IconButton
|
|
34
|
+
icon={CloseIcon}
|
|
35
|
+
variant="filled"
|
|
36
|
+
colour={defaultColour}
|
|
37
|
+
size="sm"
|
|
38
|
+
aria-label={ariaLabel}
|
|
39
|
+
onPress={closeDrawer}
|
|
40
|
+
/>
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if (!headerContext) {
|
|
44
|
+
return (
|
|
45
|
+
<View
|
|
46
|
+
ref={ref}
|
|
47
|
+
style={[
|
|
48
|
+
styles.floating,
|
|
49
|
+
{
|
|
50
|
+
top: parseTokenValue(spacing.close.top.md),
|
|
51
|
+
right: parseTokenValue(spacing.close.right.md)
|
|
52
|
+
}
|
|
53
|
+
]}
|
|
54
|
+
{...props}
|
|
55
|
+
>
|
|
56
|
+
{button}
|
|
57
|
+
</View>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<View ref={ref} {...props}>
|
|
63
|
+
{button}
|
|
64
|
+
</View>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
DrawerClose.displayName = "Drawer.Close"
|
|
70
|
+
|
|
71
|
+
const styles = StyleSheet.create({
|
|
72
|
+
floating: {
|
|
73
|
+
position: "absolute",
|
|
74
|
+
zIndex: 1
|
|
75
|
+
}
|
|
76
|
+
})
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|
2
|
+
import {
|
|
3
|
+
Animated,
|
|
4
|
+
Easing,
|
|
5
|
+
PanResponder,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
View,
|
|
8
|
+
useWindowDimensions,
|
|
9
|
+
type ViewProps
|
|
10
|
+
} from "react-native"
|
|
11
|
+
import styled from "@emotion/native"
|
|
12
|
+
import { useTheme } from "@emotion/react"
|
|
13
|
+
import { DrawerDragContext } from "./DrawerDragContext"
|
|
14
|
+
import { useDrawerContext } from "./DrawerContext"
|
|
15
|
+
import { DrawerFooterContext } from "./DrawerFooterContext"
|
|
16
|
+
import {
|
|
17
|
+
DrawerHeaderContext,
|
|
18
|
+
type DrawerHeaderVariant
|
|
19
|
+
} from "./DrawerHeaderContext"
|
|
20
|
+
|
|
21
|
+
export type DrawerContentProps = Omit<ViewProps, "style">
|
|
22
|
+
|
|
23
|
+
const OPEN_DURATION = 320
|
|
24
|
+
const CLOSE_DURATION = 220
|
|
25
|
+
const DISMISS_THRESHOLD = 80
|
|
26
|
+
|
|
27
|
+
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
28
|
+
|
|
29
|
+
const StyledPanel = styled(View)<{
|
|
30
|
+
panelBorderRadius: number
|
|
31
|
+
panelBgColor: string
|
|
32
|
+
panelMaxWidth: number
|
|
33
|
+
panelBorderWidth: number
|
|
34
|
+
panelBorderColor: string
|
|
35
|
+
panelShadowColor: string
|
|
36
|
+
panelShadowOffsetY: number
|
|
37
|
+
panelShadowBlur: number
|
|
38
|
+
panelFlex?: number
|
|
39
|
+
}>(
|
|
40
|
+
({
|
|
41
|
+
panelBorderRadius,
|
|
42
|
+
panelBgColor,
|
|
43
|
+
panelMaxWidth,
|
|
44
|
+
panelBorderWidth,
|
|
45
|
+
panelBorderColor,
|
|
46
|
+
panelShadowColor,
|
|
47
|
+
panelShadowOffsetY,
|
|
48
|
+
panelShadowBlur,
|
|
49
|
+
panelFlex
|
|
50
|
+
}) => ({
|
|
51
|
+
width: "100%",
|
|
52
|
+
maxWidth: panelMaxWidth,
|
|
53
|
+
alignSelf: "center",
|
|
54
|
+
...(panelFlex != null ? { flex: panelFlex } : undefined),
|
|
55
|
+
borderTopLeftRadius: panelBorderRadius,
|
|
56
|
+
borderTopRightRadius: panelBorderRadius,
|
|
57
|
+
borderTopWidth: panelBorderWidth,
|
|
58
|
+
borderLeftWidth: panelBorderWidth,
|
|
59
|
+
borderRightWidth: panelBorderWidth,
|
|
60
|
+
borderColor: panelBorderColor,
|
|
61
|
+
backgroundColor: panelBgColor,
|
|
62
|
+
shadowColor: panelShadowColor,
|
|
63
|
+
shadowOffset: { width: 0, height: panelShadowOffsetY },
|
|
64
|
+
shadowOpacity: 1,
|
|
65
|
+
shadowRadius: panelShadowBlur,
|
|
66
|
+
elevation: 8,
|
|
67
|
+
overflow: "hidden"
|
|
68
|
+
})
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Main panel container for the native drawer. Slides up from the bottom of
|
|
73
|
+
* the screen. Uses React Native `Animated` for entrance/exit and `PanResponder`
|
|
74
|
+
* on the grabber for drag-to-dismiss.
|
|
75
|
+
*
|
|
76
|
+
* Must be rendered inside `Drawer.Portal`.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```tsx
|
|
80
|
+
* <Drawer.Portal>
|
|
81
|
+
* <Drawer.Overlay />
|
|
82
|
+
* <Drawer.Content>
|
|
83
|
+
* <Drawer.Header variant="titleAndText">
|
|
84
|
+
* <Drawer.Title>Title</Drawer.Title>
|
|
85
|
+
* <Drawer.Close />
|
|
86
|
+
* </Drawer.Header>
|
|
87
|
+
* <Drawer.Body>Content</Drawer.Body>
|
|
88
|
+
* </Drawer.Content>
|
|
89
|
+
* </Drawer.Portal>
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export const DrawerContent = React.forwardRef<View, DrawerContentProps>(
|
|
93
|
+
({ children, ...props }, ref) => {
|
|
94
|
+
const theme = useTheme()
|
|
95
|
+
const { drawer, modal } = theme.tokens.components
|
|
96
|
+
const { colour, dimensions } = theme.tokens.semantics
|
|
97
|
+
|
|
98
|
+
const { isOpen, closeDrawer, onExitComplete } = useDrawerContext()
|
|
99
|
+
|
|
100
|
+
const { height: windowHeight } = useWindowDimensions()
|
|
101
|
+
const maxHeight = windowHeight * 0.9
|
|
102
|
+
|
|
103
|
+
// Keep a ref so the PanResponder (created once) always calls the latest
|
|
104
|
+
// closeDrawer without becoming a stale closure.
|
|
105
|
+
const closeDrawerRef = useRef(closeDrawer)
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
closeDrawerRef.current = closeDrawer
|
|
108
|
+
}, [closeDrawer])
|
|
109
|
+
|
|
110
|
+
const translateY = useRef(new Animated.Value(9999)).current
|
|
111
|
+
const translateYValue = useRef(9999)
|
|
112
|
+
const panelHeightRef = useRef(0)
|
|
113
|
+
const entryRanRef = useRef(false)
|
|
114
|
+
const isOpenRef = useRef(isOpen)
|
|
115
|
+
const activeAnim = useRef<Animated.CompositeAnimation | null>(null)
|
|
116
|
+
|
|
117
|
+
// After the first onLayout we know the real rendered height (capped at
|
|
118
|
+
// MAX_HEIGHT). We immediately kick off the entry animation from that height
|
|
119
|
+
// and then set explicit height + flex: 1 on the panel so DrawerBody's
|
|
120
|
+
// ScrollView gets a bounded container and can scroll correctly.
|
|
121
|
+
const [panelHeight, setPanelHeight] = useState(0)
|
|
122
|
+
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
const id = translateY.addListener(({ value }) => {
|
|
125
|
+
translateYValue.current = value
|
|
126
|
+
})
|
|
127
|
+
return () => translateY.removeListener(id)
|
|
128
|
+
}, [translateY])
|
|
129
|
+
|
|
130
|
+
// Clamp the cached panel height when the window shrinks (e.g. orientation
|
|
131
|
+
// change) so the entry animation never starts from below the screen.
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (panelHeightRef.current > 0 && panelHeightRef.current > maxHeight) {
|
|
134
|
+
setPanelHeight(maxHeight)
|
|
135
|
+
panelHeightRef.current = maxHeight
|
|
136
|
+
}
|
|
137
|
+
}, [maxHeight])
|
|
138
|
+
|
|
139
|
+
const runEntry = useCallback(() => {
|
|
140
|
+
if (panelHeightRef.current === 0) return
|
|
141
|
+
translateY.setValue(panelHeightRef.current)
|
|
142
|
+
activeAnim.current?.stop()
|
|
143
|
+
const anim = Animated.timing(translateY, {
|
|
144
|
+
toValue: 0,
|
|
145
|
+
duration: OPEN_DURATION,
|
|
146
|
+
easing: Easing.bezier(0.32, 0.72, 0, 1),
|
|
147
|
+
useNativeDriver: true
|
|
148
|
+
})
|
|
149
|
+
activeAnim.current = anim
|
|
150
|
+
anim.start(() => {
|
|
151
|
+
activeAnim.current = null
|
|
152
|
+
})
|
|
153
|
+
entryRanRef.current = true
|
|
154
|
+
}, [translateY])
|
|
155
|
+
|
|
156
|
+
const runExit = useCallback(() => {
|
|
157
|
+
const height = panelHeightRef.current
|
|
158
|
+
if (height === 0) {
|
|
159
|
+
onExitComplete()
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
activeAnim.current?.stop()
|
|
163
|
+
const anim = Animated.timing(translateY, {
|
|
164
|
+
toValue: height * 1.1,
|
|
165
|
+
duration: CLOSE_DURATION,
|
|
166
|
+
easing: Easing.bezier(0.32, 0.72, 0, 1),
|
|
167
|
+
useNativeDriver: true
|
|
168
|
+
})
|
|
169
|
+
activeAnim.current = anim
|
|
170
|
+
anim.start(() => {
|
|
171
|
+
activeAnim.current = null
|
|
172
|
+
onExitComplete()
|
|
173
|
+
})
|
|
174
|
+
}, [translateY, onExitComplete])
|
|
175
|
+
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
isOpenRef.current = isOpen
|
|
178
|
+
if (isOpen) {
|
|
179
|
+
entryRanRef.current = false
|
|
180
|
+
runEntry()
|
|
181
|
+
} else {
|
|
182
|
+
runExit()
|
|
183
|
+
}
|
|
184
|
+
}, [isOpen, runEntry, runExit])
|
|
185
|
+
|
|
186
|
+
const onLayout = useCallback(
|
|
187
|
+
({ nativeEvent }: { nativeEvent: { layout: { height: number } } }) => {
|
|
188
|
+
const h = nativeEvent.layout.height
|
|
189
|
+
const capped = Math.min(h, maxHeight)
|
|
190
|
+
|
|
191
|
+
// Only update state when the effective height actually changes, to
|
|
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) {
|
|
195
|
+
setPanelHeight(capped)
|
|
196
|
+
panelHeightRef.current = capped
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Run entry on the first measurement. The animation starts from the
|
|
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
|
|
205
|
+
runEntry()
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
[runEntry, maxHeight]
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
const panResponder = useRef(
|
|
212
|
+
PanResponder.create({
|
|
213
|
+
// Claim the responder as soon as a touch starts on the grabber so the
|
|
214
|
+
// gesture is never stolen by a parent ScrollView.
|
|
215
|
+
onStartShouldSetPanResponder: () => true,
|
|
216
|
+
onMoveShouldSetPanResponder: (_evt, gestureState) =>
|
|
217
|
+
Math.abs(gestureState.dy) > 5 && gestureState.dy > 0,
|
|
218
|
+
onPanResponderGrant: () => {
|
|
219
|
+
activeAnim.current?.stop()
|
|
220
|
+
activeAnim.current = null
|
|
221
|
+
},
|
|
222
|
+
onPanResponderMove: (_evt, gestureState) => {
|
|
223
|
+
const next = Math.max(0, gestureState.dy)
|
|
224
|
+
translateY.setValue(next)
|
|
225
|
+
},
|
|
226
|
+
onPanResponderRelease: (_evt, gestureState) => {
|
|
227
|
+
const currentY = translateYValue.current
|
|
228
|
+
if (currentY > DISMISS_THRESHOLD || gestureState.vy > 0.5) {
|
|
229
|
+
const dismissAnim = Animated.timing(translateY, {
|
|
230
|
+
toValue: panelHeightRef.current * 1.1,
|
|
231
|
+
duration: CLOSE_DURATION,
|
|
232
|
+
easing: Easing.bezier(0.32, 0.72, 0, 1),
|
|
233
|
+
useNativeDriver: true
|
|
234
|
+
})
|
|
235
|
+
activeAnim.current = dismissAnim
|
|
236
|
+
dismissAnim.start(() => {
|
|
237
|
+
activeAnim.current = null
|
|
238
|
+
closeDrawerRef.current()
|
|
239
|
+
})
|
|
240
|
+
} else {
|
|
241
|
+
const snapAnim = Animated.spring(translateY, {
|
|
242
|
+
toValue: 0,
|
|
243
|
+
bounciness: 8,
|
|
244
|
+
useNativeDriver: true
|
|
245
|
+
})
|
|
246
|
+
activeAnim.current = snapAnim
|
|
247
|
+
snapAnim.start(() => {
|
|
248
|
+
activeAnim.current = null
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
onPanResponderTerminate: () => {
|
|
253
|
+
const snapAnim = Animated.spring(translateY, {
|
|
254
|
+
toValue: 0,
|
|
255
|
+
bounciness: 8,
|
|
256
|
+
useNativeDriver: true
|
|
257
|
+
})
|
|
258
|
+
activeAnim.current = snapAnim
|
|
259
|
+
snapAnim.start(() => {
|
|
260
|
+
activeAnim.current = null
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
).current
|
|
265
|
+
|
|
266
|
+
const dragContextValue = useMemo(
|
|
267
|
+
() => ({ panHandlers: panResponder.panHandlers }),
|
|
268
|
+
[panResponder.panHandlers]
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
const hasFooter = React.Children.toArray(children).some(
|
|
272
|
+
(child) =>
|
|
273
|
+
React.isValidElement(child) &&
|
|
274
|
+
(child.type as { displayName?: string }).displayName === "Drawer.Footer"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
const headerChild = React.Children.toArray(children).find(
|
|
278
|
+
(child) =>
|
|
279
|
+
React.isValidElement(child) &&
|
|
280
|
+
(child.type as { displayName?: string }).displayName === "Drawer.Header"
|
|
281
|
+
) as React.ReactElement<{ variant?: DrawerHeaderVariant }> | undefined
|
|
282
|
+
|
|
283
|
+
const headerContextValue =
|
|
284
|
+
headerChild != null
|
|
285
|
+
? {
|
|
286
|
+
variant:
|
|
287
|
+
headerChild.props.variant ??
|
|
288
|
+
("titleAndText" as DrawerHeaderVariant)
|
|
289
|
+
}
|
|
290
|
+
: null
|
|
291
|
+
|
|
292
|
+
return (
|
|
293
|
+
<DrawerHeaderContext.Provider value={headerContextValue}>
|
|
294
|
+
<DrawerFooterContext.Provider value={hasFooter}>
|
|
295
|
+
<DrawerDragContext.Provider value={dragContextValue}>
|
|
296
|
+
<Animated.View
|
|
297
|
+
style={[
|
|
298
|
+
styles.animatedWrapper,
|
|
299
|
+
panelHeight > 0 ? { height: panelHeight } : undefined,
|
|
300
|
+
{ transform: [{ translateY }] }
|
|
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}
|
|
319
|
+
>
|
|
320
|
+
{children}
|
|
321
|
+
</StyledPanel>
|
|
322
|
+
</Animated.View>
|
|
323
|
+
</DrawerDragContext.Provider>
|
|
324
|
+
</DrawerFooterContext.Provider>
|
|
325
|
+
</DrawerHeaderContext.Provider>
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
DrawerContent.displayName = "Drawer.Content"
|
|
331
|
+
|
|
332
|
+
const styles = StyleSheet.create({
|
|
333
|
+
animatedWrapper: {
|
|
334
|
+
position: "absolute",
|
|
335
|
+
bottom: 0,
|
|
336
|
+
left: 0,
|
|
337
|
+
right: 0
|
|
338
|
+
}
|
|
339
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
|
|
3
|
+
export type DrawerContextValue = {
|
|
4
|
+
isOpen: boolean
|
|
5
|
+
modalVisible: boolean
|
|
6
|
+
openDrawer: () => void
|
|
7
|
+
closeDrawer: () => void
|
|
8
|
+
onExitComplete: () => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const DrawerContext = React.createContext<DrawerContextValue>({
|
|
12
|
+
isOpen: false,
|
|
13
|
+
modalVisible: false,
|
|
14
|
+
openDrawer: () => {},
|
|
15
|
+
closeDrawer: () => {},
|
|
16
|
+
onExitComplete: () => {}
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
export const useDrawerContext = () => React.useContext(DrawerContext)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { View, ViewProps } from "react-native"
|
|
3
|
+
import { useTheme } from "@emotion/react"
|
|
4
|
+
import { Typography } from "../../atoms/Typography"
|
|
5
|
+
|
|
6
|
+
export type DrawerDescriptionProps = {
|
|
7
|
+
children: React.ReactNode
|
|
8
|
+
} & Omit<ViewProps, "children">
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Description text for the drawer header.
|
|
12
|
+
*/
|
|
13
|
+
export const DrawerDescription = React.forwardRef<View, DrawerDescriptionProps>(
|
|
14
|
+
({ children, ...props }, ref) => {
|
|
15
|
+
const theme = useTheme()
|
|
16
|
+
const { drawerHeader } = theme.tokens.components.drawer
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<View ref={ref} {...props}>
|
|
20
|
+
<Typography
|
|
21
|
+
token={drawerHeader.typography.description}
|
|
22
|
+
color={drawerHeader.colour.text.description}
|
|
23
|
+
>
|
|
24
|
+
{children}
|
|
25
|
+
</Typography>
|
|
26
|
+
</View>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
DrawerDescription.displayName = "Drawer.Description"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { PanResponderInstance } from "react-native"
|
|
3
|
+
|
|
4
|
+
export type DrawerDragContextValue = {
|
|
5
|
+
panHandlers: PanResponderInstance["panHandlers"]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const DrawerDragContext =
|
|
9
|
+
React.createContext<DrawerDragContextValue | null>(null)
|
|
10
|
+
|
|
11
|
+
export const useDrawerDragContext = () => React.useContext(DrawerDragContext)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { View, ViewProps } from "react-native"
|
|
3
|
+
import styled from "@emotion/native"
|
|
4
|
+
import { useTheme } from "@emotion/react"
|
|
5
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context"
|
|
6
|
+
|
|
7
|
+
export type DrawerFooterProps = ViewProps
|
|
8
|
+
|
|
9
|
+
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
10
|
+
|
|
11
|
+
const StyledFooter = styled(View)<{
|
|
12
|
+
footerPaddingTop: number
|
|
13
|
+
footerPaddingHorizontal: number
|
|
14
|
+
footerPaddingBottom: number
|
|
15
|
+
}>(({ footerPaddingTop, footerPaddingHorizontal, footerPaddingBottom }) => ({
|
|
16
|
+
flexShrink: 0,
|
|
17
|
+
paddingTop: footerPaddingTop,
|
|
18
|
+
paddingHorizontal: footerPaddingHorizontal,
|
|
19
|
+
paddingBottom: footerPaddingBottom
|
|
20
|
+
}))
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Fixed footer section of the drawer. Typically contains action buttons.
|
|
24
|
+
*/
|
|
25
|
+
export const DrawerFooter = React.forwardRef<View, DrawerFooterProps>(
|
|
26
|
+
({ children, ...props }, ref) => {
|
|
27
|
+
const theme = useTheme()
|
|
28
|
+
const { spacing } = theme.tokens.components.drawer
|
|
29
|
+
const insets = useSafeAreaInsets()
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<StyledFooter
|
|
33
|
+
ref={ref}
|
|
34
|
+
footerPaddingTop={parseTokenValue(spacing.footer.topPaddding)}
|
|
35
|
+
footerPaddingHorizontal={parseTokenValue(
|
|
36
|
+
spacing.footer.horizontalPadding
|
|
37
|
+
)}
|
|
38
|
+
footerPaddingBottom={
|
|
39
|
+
parseTokenValue(spacing.bottomPadding) + insets.bottom
|
|
40
|
+
}
|
|
41
|
+
{...props}
|
|
42
|
+
>
|
|
43
|
+
{children}
|
|
44
|
+
</StyledFooter>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
DrawerFooter.displayName = "Drawer.Footer"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { View, ViewProps } from "react-native"
|
|
3
|
+
import styled from "@emotion/native"
|
|
4
|
+
import { useTheme } from "@emotion/react"
|
|
5
|
+
import { useDrawerDragContext } from "./DrawerDragContext"
|
|
6
|
+
|
|
7
|
+
export type DrawerGrabberProps = ViewProps
|
|
8
|
+
|
|
9
|
+
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
10
|
+
|
|
11
|
+
const StyledWrapper = styled(View)<{ grabberPaddingTop: number }>(
|
|
12
|
+
({ grabberPaddingTop }) => ({
|
|
13
|
+
alignItems: "center",
|
|
14
|
+
justifyContent: "center",
|
|
15
|
+
width: "100%",
|
|
16
|
+
paddingTop: grabberPaddingTop,
|
|
17
|
+
paddingBottom: grabberPaddingTop
|
|
18
|
+
})
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
const StyledBar = styled(View)<{
|
|
22
|
+
barWidth: number
|
|
23
|
+
barHeight: number
|
|
24
|
+
barBorderRadius: number
|
|
25
|
+
barColor: string
|
|
26
|
+
}>(({ barWidth, barHeight, barBorderRadius, barColor }) => ({
|
|
27
|
+
width: barWidth,
|
|
28
|
+
height: barHeight,
|
|
29
|
+
borderRadius: barBorderRadius,
|
|
30
|
+
backgroundColor: barColor
|
|
31
|
+
}))
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Standalone pull handle for the drawer. Attach to the top of `Drawer.Content`
|
|
35
|
+
* when `Drawer.Header` is not used. Activates drag-to-dismiss.
|
|
36
|
+
*/
|
|
37
|
+
export const DrawerGrabber = React.forwardRef<View, DrawerGrabberProps>(
|
|
38
|
+
(props, ref) => {
|
|
39
|
+
const theme = useTheme()
|
|
40
|
+
const dragContext = useDrawerDragContext()
|
|
41
|
+
const { drawer, grabber } = theme.tokens.components
|
|
42
|
+
const { borderRadius } = theme.tokens.semantics.dimensions
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<StyledWrapper
|
|
46
|
+
ref={ref}
|
|
47
|
+
grabberPaddingTop={parseTokenValue(drawer.spacing.grabber.paddingTop)}
|
|
48
|
+
{...(dragContext?.panHandlers ?? {})}
|
|
49
|
+
{...props}
|
|
50
|
+
>
|
|
51
|
+
<StyledBar
|
|
52
|
+
barWidth={parseTokenValue(grabber.size.bar.width)}
|
|
53
|
+
barHeight={parseTokenValue(grabber.size.bar.height)}
|
|
54
|
+
barBorderRadius={parseTokenValue(borderRadius.round)}
|
|
55
|
+
barColor={grabber.colour.background.default}
|
|
56
|
+
/>
|
|
57
|
+
</StyledWrapper>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
DrawerGrabber.displayName = "Drawer.Grabber"
|