@korsolutions/ui 0.0.19 → 0.0.20
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/dist/components/index.d.mts +42 -4
- package/dist/components/index.mjs +119 -8
- package/dist/{index-BLsiF42Z.d.mts → index-CGY0mO6z.d.mts} +172 -9
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +2 -2
- package/dist/primitives/index.d.mts +2 -2
- package/dist/primitives/index.mjs +2 -2
- package/dist/{primitives-CyDqzNcp.mjs → primitives-P_8clvQr.mjs} +433 -13
- package/dist/{toast-manager-BOORCQn8.mjs → toast-manager-DSo9oN8w.mjs} +1 -1
- package/package.json +1 -1
- package/src/components/button/button.tsx +7 -4
- package/src/components/dropdown-menu/dropdown-menu.tsx +49 -0
- package/src/components/dropdown-menu/variants/default.tsx +40 -0
- package/src/components/dropdown-menu/variants/index.ts +5 -0
- package/src/components/index.ts +2 -0
- package/src/components/popover/popover.tsx +51 -0
- package/src/components/popover/variants/default.tsx +26 -0
- package/src/components/popover/variants/index.ts +5 -0
- package/src/hooks/useRelativePosition.ts +188 -0
- package/src/primitives/button/button-root.tsx +2 -4
- package/src/primitives/dropdown-menu/context.ts +25 -0
- package/src/primitives/dropdown-menu/dropdown-menu-button.tsx +47 -0
- package/src/primitives/dropdown-menu/dropdown-menu-content.tsx +39 -0
- package/src/primitives/dropdown-menu/dropdown-menu-divider.tsx +18 -0
- package/src/primitives/dropdown-menu/dropdown-menu-overlay.tsx +29 -0
- package/src/primitives/dropdown-menu/dropdown-menu-portal.tsx +21 -0
- package/src/primitives/dropdown-menu/dropdown-menu-root.tsx +35 -0
- package/src/primitives/dropdown-menu/dropdown-menu-trigger.tsx +47 -0
- package/src/primitives/dropdown-menu/index.ts +26 -0
- package/src/primitives/dropdown-menu/types.ts +13 -0
- package/src/primitives/index.ts +2 -0
- package/src/primitives/popover/context.ts +25 -0
- package/src/primitives/popover/index.ts +24 -0
- package/src/primitives/popover/popover-close.tsx +29 -0
- package/src/primitives/popover/popover-content.tsx +39 -0
- package/src/primitives/popover/popover-overlay.tsx +37 -0
- package/src/primitives/popover/popover-portal.tsx +21 -0
- package/src/primitives/popover/popover-root.tsx +35 -0
- package/src/primitives/popover/popover-trigger.tsx +47 -0
- package/src/primitives/popover/types.ts +7 -0
- package/src/utils/get-ref-layout.ts +16 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { PopoverPrimitive, usePopover } from "@/primitives";
|
|
2
|
+
import React, { forwardRef } from "react";
|
|
3
|
+
import { PopoverVariants } from "./variants";
|
|
4
|
+
import type { PopoverTriggerRef } from "@/primitives";
|
|
5
|
+
import { PressableProps } from "react-native";
|
|
6
|
+
|
|
7
|
+
interface PopoverChildrenProps {
|
|
8
|
+
close: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface PopoverProps {
|
|
12
|
+
children: ((props: PopoverChildrenProps) => React.ReactNode) | React.ReactNode;
|
|
13
|
+
trigger: React.ReactElement<PressableProps>;
|
|
14
|
+
closeOnOverlayPress?: boolean;
|
|
15
|
+
|
|
16
|
+
variant?: keyof typeof PopoverVariants;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const PopoverContentComponent = (props: { children: PopoverProps["children"] }) => {
|
|
20
|
+
const popover = usePopover();
|
|
21
|
+
if (typeof props.children === "function") {
|
|
22
|
+
return (
|
|
23
|
+
<PopoverPrimitive.Content>
|
|
24
|
+
{props.children({
|
|
25
|
+
close: () => popover.setIsOpen(false),
|
|
26
|
+
})}
|
|
27
|
+
</PopoverPrimitive.Content>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return <PopoverPrimitive.Content>{props.children}</PopoverPrimitive.Content>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const PopoverComponent = forwardRef<PopoverTriggerRef, PopoverProps>((props, ref) => {
|
|
34
|
+
const useVariantStyles = PopoverVariants[props.variant || "default"];
|
|
35
|
+
const styles = useVariantStyles();
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<PopoverPrimitive.Root styles={styles}>
|
|
39
|
+
<PopoverPrimitive.Trigger ref={ref}>{props.trigger}</PopoverPrimitive.Trigger>
|
|
40
|
+
<PopoverPrimitive.Portal>
|
|
41
|
+
<PopoverPrimitive.Overlay closeOnPress={props.closeOnOverlayPress}>
|
|
42
|
+
<PopoverContentComponent>{props.children}</PopoverContentComponent>
|
|
43
|
+
</PopoverPrimitive.Overlay>
|
|
44
|
+
</PopoverPrimitive.Portal>
|
|
45
|
+
</PopoverPrimitive.Root>
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
PopoverComponent.displayName = "Popover";
|
|
50
|
+
|
|
51
|
+
export const Popover = PopoverComponent;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { PopoverStyles } from "@/primitives/popover/types";
|
|
2
|
+
import { useThemedStyles } from "@/utils/use-themed-styles";
|
|
3
|
+
|
|
4
|
+
export const usePopoverVariantDefault = (): PopoverStyles => {
|
|
5
|
+
return useThemedStyles(
|
|
6
|
+
({ colors, radius }): PopoverStyles => ({
|
|
7
|
+
overlay: {
|
|
8
|
+
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
|
9
|
+
},
|
|
10
|
+
content: {
|
|
11
|
+
backgroundColor: colors.surface,
|
|
12
|
+
borderRadius: radius,
|
|
13
|
+
borderWidth: 1,
|
|
14
|
+
borderColor: colors.border,
|
|
15
|
+
padding: 16,
|
|
16
|
+
minWidth: 280,
|
|
17
|
+
maxWidth: 320,
|
|
18
|
+
shadowColor: "#000",
|
|
19
|
+
shadowOffset: { width: 0, height: 4 },
|
|
20
|
+
shadowOpacity: 0.15,
|
|
21
|
+
shadowRadius: 12,
|
|
22
|
+
elevation: 8,
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
);
|
|
26
|
+
};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { useWindowDimensions, type LayoutRectangle, type ScaledSize } from "react-native";
|
|
3
|
+
|
|
4
|
+
interface Insets {
|
|
5
|
+
top?: number;
|
|
6
|
+
bottom?: number;
|
|
7
|
+
left?: number;
|
|
8
|
+
right?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type UseRelativePositionArgs = Omit<GetContentStyleArgs, "triggerPosition" | "contentLayout" | "dimensions"> & {
|
|
12
|
+
triggerPosition: LayoutPosition | null;
|
|
13
|
+
contentLayout: LayoutRectangle | null;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function useRelativePosition({
|
|
17
|
+
align,
|
|
18
|
+
avoidCollisions,
|
|
19
|
+
triggerPosition,
|
|
20
|
+
contentLayout,
|
|
21
|
+
alignOffset,
|
|
22
|
+
insets,
|
|
23
|
+
sideOffset,
|
|
24
|
+
side,
|
|
25
|
+
}: UseRelativePositionArgs) {
|
|
26
|
+
const dimensions = useWindowDimensions();
|
|
27
|
+
|
|
28
|
+
return React.useMemo(() => {
|
|
29
|
+
if (!triggerPosition || !contentLayout) {
|
|
30
|
+
return {
|
|
31
|
+
position: "absolute",
|
|
32
|
+
opacity: 0,
|
|
33
|
+
top: dimensions.height,
|
|
34
|
+
zIndex: -9999999,
|
|
35
|
+
} as const;
|
|
36
|
+
}
|
|
37
|
+
return getContentStyle({
|
|
38
|
+
align,
|
|
39
|
+
avoidCollisions,
|
|
40
|
+
contentLayout,
|
|
41
|
+
side,
|
|
42
|
+
triggerPosition,
|
|
43
|
+
alignOffset,
|
|
44
|
+
insets,
|
|
45
|
+
sideOffset,
|
|
46
|
+
dimensions,
|
|
47
|
+
});
|
|
48
|
+
}, [align, avoidCollisions, side, alignOffset, insets, triggerPosition, contentLayout, dimensions.width, dimensions.height]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface LayoutPosition {
|
|
52
|
+
pageY: number;
|
|
53
|
+
pageX: number;
|
|
54
|
+
width: number;
|
|
55
|
+
height: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface GetPositionArgs {
|
|
59
|
+
dimensions: ScaledSize;
|
|
60
|
+
avoidCollisions: boolean;
|
|
61
|
+
triggerPosition: LayoutPosition;
|
|
62
|
+
contentLayout: LayoutRectangle;
|
|
63
|
+
insets?: Insets;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface GetSidePositionArgs extends GetPositionArgs {
|
|
67
|
+
side: "top" | "bottom";
|
|
68
|
+
sideOffset: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const DEFAULT_LAYOUT: LayoutRectangle = { x: 0, y: 0, width: 0, height: 0 };
|
|
72
|
+
export const DEFAULT_POSITION: LayoutPosition = { height: 0, width: 0, pageX: 0, pageY: 0 };
|
|
73
|
+
|
|
74
|
+
function getSidePosition({ side, triggerPosition, contentLayout, sideOffset, insets, avoidCollisions, dimensions }: GetSidePositionArgs) {
|
|
75
|
+
const insetTop = insets?.top ?? 0;
|
|
76
|
+
const insetBottom = insets?.bottom ?? 0;
|
|
77
|
+
const positionTop = triggerPosition?.pageY - sideOffset - contentLayout.height;
|
|
78
|
+
const positionBottom = triggerPosition.pageY + triggerPosition.height + sideOffset;
|
|
79
|
+
|
|
80
|
+
if (!avoidCollisions) {
|
|
81
|
+
return {
|
|
82
|
+
top: side === "top" ? positionTop : positionBottom,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (side === "top") {
|
|
87
|
+
return {
|
|
88
|
+
top: Math.min(Math.max(insetTop, positionTop), dimensions.height - insetBottom - contentLayout.height),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
top: Math.min(dimensions.height - insetBottom - contentLayout.height, positionBottom),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface GetAlignPositionArgs extends GetPositionArgs {
|
|
98
|
+
align: "start" | "center" | "end";
|
|
99
|
+
alignOffset: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getAlignPosition({ align, avoidCollisions, contentLayout, triggerPosition, alignOffset, insets, dimensions }: GetAlignPositionArgs) {
|
|
103
|
+
const insetLeft = insets?.left ?? 0;
|
|
104
|
+
const insetRight = insets?.right ?? 0;
|
|
105
|
+
const maxContentWidth = dimensions.width - insetLeft - insetRight;
|
|
106
|
+
|
|
107
|
+
const contentWidth = Math.min(contentLayout.width, maxContentWidth);
|
|
108
|
+
|
|
109
|
+
let left = getLeftPosition(align, triggerPosition.pageX, triggerPosition.width, contentWidth, alignOffset, insetLeft, insetRight, dimensions);
|
|
110
|
+
|
|
111
|
+
if (avoidCollisions) {
|
|
112
|
+
const doesCollide = left < insetLeft || left + contentWidth > dimensions.width - insetRight;
|
|
113
|
+
if (doesCollide) {
|
|
114
|
+
const spaceLeft = left - insetLeft;
|
|
115
|
+
const spaceRight = dimensions.width - insetRight - (left + contentWidth);
|
|
116
|
+
|
|
117
|
+
if (spaceLeft > spaceRight && spaceLeft >= contentWidth) {
|
|
118
|
+
left = insetLeft;
|
|
119
|
+
} else if (spaceRight >= contentWidth) {
|
|
120
|
+
left = dimensions.width - insetRight - contentWidth;
|
|
121
|
+
} else {
|
|
122
|
+
const centeredPosition = Math.max(insetLeft, (dimensions.width - contentWidth - insetRight) / 2);
|
|
123
|
+
left = centeredPosition;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { left, maxWidth: maxContentWidth };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getLeftPosition(
|
|
132
|
+
align: "start" | "center" | "end",
|
|
133
|
+
triggerPageX: number,
|
|
134
|
+
triggerWidth: number,
|
|
135
|
+
contentWidth: number,
|
|
136
|
+
alignOffset: number,
|
|
137
|
+
insetLeft: number,
|
|
138
|
+
insetRight: number,
|
|
139
|
+
dimensions: ScaledSize
|
|
140
|
+
) {
|
|
141
|
+
let left = 0;
|
|
142
|
+
if (align === "start") {
|
|
143
|
+
left = triggerPageX;
|
|
144
|
+
}
|
|
145
|
+
if (align === "center") {
|
|
146
|
+
left = triggerPageX + triggerWidth / 2 - contentWidth / 2;
|
|
147
|
+
}
|
|
148
|
+
if (align === "end") {
|
|
149
|
+
left = triggerPageX + triggerWidth - contentWidth;
|
|
150
|
+
}
|
|
151
|
+
return Math.max(insetLeft, Math.min(left + alignOffset, dimensions.width - contentWidth - insetRight));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
type GetContentStyleArgs = GetPositionArgs & GetSidePositionArgs & GetAlignPositionArgs;
|
|
155
|
+
|
|
156
|
+
function getContentStyle({
|
|
157
|
+
align,
|
|
158
|
+
avoidCollisions,
|
|
159
|
+
contentLayout,
|
|
160
|
+
side,
|
|
161
|
+
triggerPosition,
|
|
162
|
+
alignOffset,
|
|
163
|
+
insets,
|
|
164
|
+
sideOffset,
|
|
165
|
+
dimensions,
|
|
166
|
+
}: GetContentStyleArgs) {
|
|
167
|
+
return Object.assign(
|
|
168
|
+
{ position: "absolute" } as const,
|
|
169
|
+
getSidePosition({
|
|
170
|
+
side,
|
|
171
|
+
triggerPosition,
|
|
172
|
+
contentLayout,
|
|
173
|
+
sideOffset,
|
|
174
|
+
insets,
|
|
175
|
+
avoidCollisions,
|
|
176
|
+
dimensions,
|
|
177
|
+
}),
|
|
178
|
+
getAlignPosition({
|
|
179
|
+
align,
|
|
180
|
+
avoidCollisions,
|
|
181
|
+
triggerPosition,
|
|
182
|
+
contentLayout,
|
|
183
|
+
alignOffset,
|
|
184
|
+
insets,
|
|
185
|
+
dimensions,
|
|
186
|
+
})
|
|
187
|
+
);
|
|
188
|
+
}
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import React, { useState } from "react";
|
|
2
|
-
import { Pressable, StyleProp, ViewStyle } from "react-native";
|
|
2
|
+
import { Pressable, PressableProps, StyleProp, ViewStyle } from "react-native";
|
|
3
3
|
import { ButtonStyles, ButtonState } from "./types";
|
|
4
4
|
import { ButtonPrimitiveContext } from "./button-context";
|
|
5
5
|
|
|
6
|
-
export interface ButtonPrimitiveRootProps {
|
|
6
|
+
export interface ButtonPrimitiveRootProps extends PressableProps {
|
|
7
7
|
children?: React.ReactNode;
|
|
8
8
|
|
|
9
|
-
onPress?: () => void;
|
|
10
|
-
|
|
11
9
|
isDisabled?: boolean;
|
|
12
10
|
isLoading?: boolean;
|
|
13
11
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createContext, Dispatch, useContext } from "react";
|
|
2
|
+
import { LayoutRectangle } from "react-native";
|
|
3
|
+
import { DropdownMenuStyles } from "./types";
|
|
4
|
+
import { LayoutPosition } from "@/hooks/useRelativePosition";
|
|
5
|
+
|
|
6
|
+
export interface DropdownMenuContext {
|
|
7
|
+
isOpen: boolean;
|
|
8
|
+
setIsOpen: Dispatch<React.SetStateAction<boolean>>;
|
|
9
|
+
triggerPosition: LayoutPosition;
|
|
10
|
+
setTriggerPosition: Dispatch<React.SetStateAction<LayoutPosition>>;
|
|
11
|
+
contentLayout: LayoutRectangle;
|
|
12
|
+
setContentLayout: Dispatch<React.SetStateAction<LayoutRectangle>>;
|
|
13
|
+
|
|
14
|
+
styles?: DropdownMenuStyles;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const DropdownMenuContext = createContext<DropdownMenuContext | undefined>(undefined);
|
|
18
|
+
|
|
19
|
+
export const useDropdownMenu = () => {
|
|
20
|
+
const context = useContext(DropdownMenuContext);
|
|
21
|
+
if (!context) {
|
|
22
|
+
throw new Error("useDropdownMenu must be used within a DropdownMenuRoot");
|
|
23
|
+
}
|
|
24
|
+
return context;
|
|
25
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { StyleProp, Text, TextStyle } from "react-native";
|
|
3
|
+
import { useDropdownMenu } from "./context";
|
|
4
|
+
import { DropdownMenuButtonState } from "./types";
|
|
5
|
+
|
|
6
|
+
export interface DropdownMenuButtonProps {
|
|
7
|
+
children?: string;
|
|
8
|
+
onPress?: () => void;
|
|
9
|
+
onMouseEnter?: () => void;
|
|
10
|
+
onMouseLeave?: () => void;
|
|
11
|
+
|
|
12
|
+
render?: (props: DropdownMenuButtonProps) => React.ReactElement;
|
|
13
|
+
|
|
14
|
+
style?: StyleProp<TextStyle>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const calculateState = (isHovered: boolean): DropdownMenuButtonState => {
|
|
18
|
+
if (isHovered) {
|
|
19
|
+
return "hovered";
|
|
20
|
+
}
|
|
21
|
+
return "default";
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function DropdownMenuButton(props: DropdownMenuButtonProps) {
|
|
25
|
+
const menu = useDropdownMenu();
|
|
26
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
27
|
+
const state = calculateState(isHovered);
|
|
28
|
+
const composedStyle = [menu.styles?.button?.default, menu.styles?.button?.[state], props.style];
|
|
29
|
+
|
|
30
|
+
const handlePress = () => {
|
|
31
|
+
props.onPress?.();
|
|
32
|
+
menu.setIsOpen((prev) => !prev);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const Component = props.render ?? Text;
|
|
36
|
+
return (
|
|
37
|
+
<Component
|
|
38
|
+
{...props}
|
|
39
|
+
onPress={handlePress}
|
|
40
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
41
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
42
|
+
style={composedStyle}
|
|
43
|
+
>
|
|
44
|
+
{props.children}
|
|
45
|
+
</Component>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { StyleProp, View, ViewStyle } from "react-native";
|
|
3
|
+
import { useDropdownMenu } from "./context";
|
|
4
|
+
import { useRelativePosition } from "@/hooks/useRelativePosition";
|
|
5
|
+
|
|
6
|
+
export interface DropdownMenuContentProps {
|
|
7
|
+
children?: React.ReactNode;
|
|
8
|
+
|
|
9
|
+
render?: (props: DropdownMenuContentProps) => React.ReactNode;
|
|
10
|
+
|
|
11
|
+
style?: StyleProp<ViewStyle>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function DropdownMenuContent(props: DropdownMenuContentProps) {
|
|
15
|
+
const menu = useDropdownMenu();
|
|
16
|
+
|
|
17
|
+
const positionStyle = useRelativePosition({
|
|
18
|
+
align: "start",
|
|
19
|
+
avoidCollisions: true,
|
|
20
|
+
triggerPosition: menu.triggerPosition,
|
|
21
|
+
contentLayout: menu.contentLayout,
|
|
22
|
+
alignOffset: 0,
|
|
23
|
+
side: "bottom",
|
|
24
|
+
sideOffset: 0,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const composedStyle = [positionStyle, menu.styles?.content, props.style];
|
|
28
|
+
|
|
29
|
+
const Component = props.render ?? View;
|
|
30
|
+
return (
|
|
31
|
+
<Component
|
|
32
|
+
{...props}
|
|
33
|
+
onLayout={(e) => {
|
|
34
|
+
menu.setContentLayout(e.nativeEvent.layout);
|
|
35
|
+
}}
|
|
36
|
+
style={composedStyle}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { StyleProp, View, ViewStyle } from "react-native";
|
|
3
|
+
import { useDropdownMenu } from "./context";
|
|
4
|
+
|
|
5
|
+
export interface DropdownMenuDividerProps {
|
|
6
|
+
render?: (props: DropdownMenuDividerProps) => React.ReactNode;
|
|
7
|
+
|
|
8
|
+
style?: StyleProp<ViewStyle>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function DropdownMenuDivider(props: DropdownMenuDividerProps) {
|
|
12
|
+
const menu = useDropdownMenu();
|
|
13
|
+
|
|
14
|
+
const composedStyle = [menu.styles?.divider, props.style];
|
|
15
|
+
|
|
16
|
+
const Component = props.render ?? View;
|
|
17
|
+
return <Component {...props} style={composedStyle} />;
|
|
18
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useDropdownMenu } from "./context";
|
|
3
|
+
import { Pressable, StyleProp, StyleSheet, ViewStyle } from "react-native";
|
|
4
|
+
|
|
5
|
+
export interface DropdownMenuOverlayProps {
|
|
6
|
+
children?: React.ReactNode;
|
|
7
|
+
|
|
8
|
+
render?: (props: DropdownMenuOverlayProps) => React.ReactElement;
|
|
9
|
+
|
|
10
|
+
style?: StyleProp<ViewStyle>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function DropdownMenuOverlay(props: DropdownMenuOverlayProps) {
|
|
14
|
+
const menu = useDropdownMenu();
|
|
15
|
+
|
|
16
|
+
const composedStyle = [StyleSheet.absoluteFill, menu.styles?.overlay, props.style];
|
|
17
|
+
|
|
18
|
+
const Component = props.render ?? Pressable;
|
|
19
|
+
return (
|
|
20
|
+
<Component
|
|
21
|
+
onPress={() => {
|
|
22
|
+
menu.setIsOpen(false);
|
|
23
|
+
}}
|
|
24
|
+
style={composedStyle}
|
|
25
|
+
>
|
|
26
|
+
{props.children}
|
|
27
|
+
</Component>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Portal } from "../portal";
|
|
3
|
+
import { useDropdownMenu, DropdownMenuContext } from "./context";
|
|
4
|
+
|
|
5
|
+
export interface DropdownMenuPortalProps {
|
|
6
|
+
children?: React.ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function DropdownMenuPortal(props: DropdownMenuPortalProps) {
|
|
10
|
+
const menu = useDropdownMenu();
|
|
11
|
+
|
|
12
|
+
if (!menu.isOpen) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<Portal name="dropdown-menu-portal">
|
|
18
|
+
<DropdownMenuContext.Provider value={menu}>{props.children}</DropdownMenuContext.Provider>
|
|
19
|
+
</Portal>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { LayoutRectangle } from "react-native";
|
|
3
|
+
import { DropdownMenuStyles } from "./types";
|
|
4
|
+
import { DropdownMenuContext } from "./context";
|
|
5
|
+
import { DEFAULT_LAYOUT, DEFAULT_POSITION, LayoutPosition } from "@/hooks/useRelativePosition";
|
|
6
|
+
|
|
7
|
+
export interface DropdownMenuRootProps {
|
|
8
|
+
children?: React.ReactNode;
|
|
9
|
+
|
|
10
|
+
render?: (props: DropdownMenuRootProps) => React.ReactNode;
|
|
11
|
+
|
|
12
|
+
styles?: DropdownMenuStyles;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function DropdownMenuRoot(props: DropdownMenuRootProps) {
|
|
16
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
17
|
+
const [triggerPosition, setTriggerPosition] = useState<LayoutPosition>(DEFAULT_POSITION);
|
|
18
|
+
const [contentLayout, setContentLayout] = useState<LayoutRectangle>(DEFAULT_LAYOUT);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<DropdownMenuContext.Provider
|
|
22
|
+
value={{
|
|
23
|
+
isOpen,
|
|
24
|
+
setIsOpen,
|
|
25
|
+
triggerPosition,
|
|
26
|
+
setTriggerPosition,
|
|
27
|
+
contentLayout,
|
|
28
|
+
setContentLayout,
|
|
29
|
+
styles: props.styles,
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
{props.children}
|
|
33
|
+
</DropdownMenuContext.Provider>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React, { forwardRef, RefAttributes, useImperativeHandle, useRef } from "react";
|
|
2
|
+
import { PressableProps, View } from "react-native";
|
|
3
|
+
import { useDropdownMenu } from "./context";
|
|
4
|
+
|
|
5
|
+
export interface DropdownMenuTriggerProps extends PressableProps {
|
|
6
|
+
children: React.ReactElement<RefAttributes<View> & PressableProps>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface DropdownMenuTriggerRef {
|
|
10
|
+
open: () => void;
|
|
11
|
+
close: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const DropdownMenuTrigger = forwardRef<DropdownMenuTriggerRef, DropdownMenuTriggerProps>((props, ref) => {
|
|
15
|
+
const dropdownMenu = useDropdownMenu();
|
|
16
|
+
const triggerRef = useRef<View>(null);
|
|
17
|
+
|
|
18
|
+
const onTriggerPress = async () => {
|
|
19
|
+
triggerRef.current?.measure((_x, _y, width, height, pageX, pageY) => {
|
|
20
|
+
dropdownMenu.setTriggerPosition({
|
|
21
|
+
height,
|
|
22
|
+
width,
|
|
23
|
+
pageX,
|
|
24
|
+
pageY,
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
dropdownMenu.setIsOpen((prev) => !prev);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
useImperativeHandle(ref, () => ({
|
|
32
|
+
open: () => dropdownMenu.setIsOpen(true),
|
|
33
|
+
close: () => dropdownMenu.setIsOpen(false),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
return React.cloneElement(props.children, {
|
|
37
|
+
ref: triggerRef,
|
|
38
|
+
onPress: onTriggerPress,
|
|
39
|
+
role: "button",
|
|
40
|
+
accessible: true,
|
|
41
|
+
accessibilityRole: "button",
|
|
42
|
+
accessibilityState: { expanded: dropdownMenu.isOpen },
|
|
43
|
+
...props.children.props,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
DropdownMenuTrigger.displayName = "DropdownMenuTrigger";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { DropdownMenuRoot } from "./dropdown-menu-root";
|
|
2
|
+
import { DropdownMenuTrigger } from "./dropdown-menu-trigger";
|
|
3
|
+
import { DropdownMenuContent } from "./dropdown-menu-content";
|
|
4
|
+
import { DropdownMenuButton } from "./dropdown-menu-button";
|
|
5
|
+
import { DropdownMenuDivider } from "./dropdown-menu-divider";
|
|
6
|
+
import { DropdownMenuPortal } from "./dropdown-menu-portal";
|
|
7
|
+
import { DropdownMenuOverlay } from "./dropdown-menu-overlay";
|
|
8
|
+
|
|
9
|
+
export const DropdownMenuPrimitive = {
|
|
10
|
+
Root: DropdownMenuRoot,
|
|
11
|
+
Trigger: DropdownMenuTrigger,
|
|
12
|
+
Portal: DropdownMenuPortal,
|
|
13
|
+
Overlay: DropdownMenuOverlay,
|
|
14
|
+
Content: DropdownMenuContent,
|
|
15
|
+
Button: DropdownMenuButton,
|
|
16
|
+
Divider: DropdownMenuDivider,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type { DropdownMenuRootProps } from "./dropdown-menu-root";
|
|
20
|
+
export type { DropdownMenuTriggerProps } from "./dropdown-menu-trigger";
|
|
21
|
+
export type { DropdownMenuPortalProps } from "./dropdown-menu-portal";
|
|
22
|
+
export type { DropdownMenuOverlayProps } from "./dropdown-menu-overlay";
|
|
23
|
+
export type { DropdownMenuContentProps } from "./dropdown-menu-content";
|
|
24
|
+
export type { DropdownMenuButtonProps } from "./dropdown-menu-button";
|
|
25
|
+
export type { DropdownMenuDividerProps } from "./dropdown-menu-divider";
|
|
26
|
+
export type { DropdownMenuStyles } from "./types";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { DropdownMenuContentProps } from "./dropdown-menu-content";
|
|
2
|
+
import { DropdownMenuButtonProps } from "./dropdown-menu-button";
|
|
3
|
+
import { DropdownMenuDividerProps } from "./dropdown-menu-divider";
|
|
4
|
+
import { DropdownMenuOverlayProps } from "./dropdown-menu-overlay";
|
|
5
|
+
|
|
6
|
+
export type DropdownMenuButtonState = "default" | "hovered";
|
|
7
|
+
|
|
8
|
+
export interface DropdownMenuStyles {
|
|
9
|
+
content?: DropdownMenuContentProps["style"];
|
|
10
|
+
button?: Partial<Record<DropdownMenuButtonState, DropdownMenuButtonProps["style"]>>;
|
|
11
|
+
divider?: DropdownMenuDividerProps["style"];
|
|
12
|
+
overlay?: DropdownMenuOverlayProps["style"];
|
|
13
|
+
}
|
package/src/primitives/index.ts
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createContext, Dispatch, useContext } from "react";
|
|
2
|
+
import { PopoverStyles } from "./types";
|
|
3
|
+
import { LayoutRectangle } from "react-native";
|
|
4
|
+
import { LayoutPosition } from "@/hooks/useRelativePosition";
|
|
5
|
+
|
|
6
|
+
export interface PopoverContext {
|
|
7
|
+
isOpen: boolean;
|
|
8
|
+
setIsOpen: Dispatch<React.SetStateAction<boolean>>;
|
|
9
|
+
contentLayout: LayoutRectangle;
|
|
10
|
+
setContentLayout: Dispatch<React.SetStateAction<LayoutRectangle>>;
|
|
11
|
+
triggerPosition: LayoutPosition;
|
|
12
|
+
setTriggerPosition: Dispatch<React.SetStateAction<LayoutPosition>>;
|
|
13
|
+
|
|
14
|
+
styles?: PopoverStyles;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const PopoverContext = createContext<PopoverContext | undefined>(undefined);
|
|
18
|
+
|
|
19
|
+
export const usePopover = () => {
|
|
20
|
+
const context = useContext(PopoverContext);
|
|
21
|
+
if (!context) {
|
|
22
|
+
throw new Error("usePopover must be used within a PopoverRoot");
|
|
23
|
+
}
|
|
24
|
+
return context;
|
|
25
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { PopoverRoot } from "./popover-root";
|
|
2
|
+
import { PopoverTrigger } from "./popover-trigger";
|
|
3
|
+
import { PopoverPortal } from "./popover-portal";
|
|
4
|
+
import { PopoverOverlay } from "./popover-overlay";
|
|
5
|
+
import { PopoverContent } from "./popover-content";
|
|
6
|
+
import { PopoverClose } from "./popover-close";
|
|
7
|
+
|
|
8
|
+
export const PopoverPrimitive = {
|
|
9
|
+
Root: PopoverRoot,
|
|
10
|
+
Trigger: PopoverTrigger,
|
|
11
|
+
Portal: PopoverPortal,
|
|
12
|
+
Overlay: PopoverOverlay,
|
|
13
|
+
Content: PopoverContent,
|
|
14
|
+
Close: PopoverClose,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type { PopoverRootProps } from "./popover-root";
|
|
18
|
+
export type { PopoverTriggerProps, PopoverTriggerRef } from "./popover-trigger";
|
|
19
|
+
export type { PopoverPortalProps } from "./popover-portal";
|
|
20
|
+
export type { PopoverOverlayProps } from "./popover-overlay";
|
|
21
|
+
export type { PopoverContentProps } from "./popover-content";
|
|
22
|
+
export type { PopoverCloseProps } from "./popover-close";
|
|
23
|
+
export type { PopoverContext } from "./context";
|
|
24
|
+
export { usePopover } from "./context";
|