react-native-molecules 0.5.0-beta.17 → 0.5.0-beta.18
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/components/Menu/Menu.tsx +3 -18
- package/components/Popover/Popover.tsx +121 -122
- package/components/Popover/PopoverRoot.tsx +74 -0
- package/components/Popover/common.ts +50 -34
- package/components/Popover/index.ts +18 -1
- package/components/Popover/{Popover.native.tsx → usePlatformMeasure.native.ts} +12 -86
- package/components/Popover/usePlatformMeasure.ts +118 -0
- package/components/Popover/utils.ts +2 -9
- package/package.json +1 -1
package/components/Menu/Menu.tsx
CHANGED
|
@@ -9,6 +9,7 @@ export type Props = Omit<PopoverProps, 'setIsOpen' | 'onClose' | 'children'> & {
|
|
|
9
9
|
closeOnSelect?: boolean;
|
|
10
10
|
onClose: () => void;
|
|
11
11
|
children: ReactElement | ReactElement[];
|
|
12
|
+
backdropStyles?: ViewStyle;
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
const emptyObj = {} as ViewStyle;
|
|
@@ -37,25 +38,9 @@ const Menu = ({
|
|
|
37
38
|
[closeOnSelect, onClose],
|
|
38
39
|
);
|
|
39
40
|
|
|
40
|
-
// const { menuItemsLength } = useMemo(
|
|
41
|
-
// () => ({
|
|
42
|
-
// menuItemsLength:
|
|
43
|
-
// Children.map(children, child => child)?.filter(
|
|
44
|
-
// child => (child.type as FC)?.displayName === 'MenuItem',
|
|
45
|
-
// )?.length || 0,
|
|
46
|
-
// }),
|
|
47
|
-
// [children],
|
|
48
|
-
// );
|
|
49
|
-
//
|
|
50
|
-
// console.log({ menuItemsLength });
|
|
51
|
-
|
|
52
41
|
return (
|
|
53
|
-
<Popover
|
|
54
|
-
|
|
55
|
-
onClose={onClose}
|
|
56
|
-
backdropStyles={backdropStyle}
|
|
57
|
-
style={style}
|
|
58
|
-
{...rest}>
|
|
42
|
+
<Popover isOpen={isOpen} onClose={onClose} style={style} {...rest}>
|
|
43
|
+
<Popover.Overlay style={backdropStyle} />
|
|
59
44
|
<MenuContext.Provider value={contextValue}>{children}</MenuContext.Provider>
|
|
60
45
|
</Popover>
|
|
61
46
|
);
|
|
@@ -1,18 +1,37 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
cloneElement,
|
|
3
|
+
Fragment,
|
|
4
|
+
memo,
|
|
5
|
+
type ReactElement,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
type Ref,
|
|
8
|
+
useContext,
|
|
9
|
+
useMemo,
|
|
10
|
+
useRef,
|
|
11
|
+
} from 'react';
|
|
12
|
+
import { Pressable, type PressableProps, type StyleProp, View, type ViewStyle } from 'react-native';
|
|
3
13
|
import { ScopedTheme, UnistylesRuntime } from 'react-native-unistyles';
|
|
4
14
|
|
|
15
|
+
import { mergeRefs } from '../../utils';
|
|
5
16
|
import { Portal } from '../Portal';
|
|
6
17
|
import {
|
|
7
18
|
DEFAULT_ARROW_SIZE,
|
|
8
|
-
|
|
19
|
+
PopoverContext,
|
|
20
|
+
PopoverPanelContext,
|
|
21
|
+
type PopoverPanelContextValue,
|
|
9
22
|
type PopoverProps,
|
|
10
23
|
useArrowStyles,
|
|
11
24
|
usePopover,
|
|
12
25
|
} from './common';
|
|
26
|
+
import { createPopoverRoot } from './PopoverRoot';
|
|
27
|
+
import { usePlatformMeasure } from './usePlatformMeasure';
|
|
13
28
|
import { popoverStyles } from './utils';
|
|
14
29
|
|
|
15
|
-
|
|
30
|
+
type PopoverPanelProps = PopoverProps & {
|
|
31
|
+
overlay?: ReactNode;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const PopoverPanel = ({
|
|
16
35
|
triggerRef,
|
|
17
36
|
children,
|
|
18
37
|
isOpen,
|
|
@@ -20,17 +39,15 @@ const Popover = ({
|
|
|
20
39
|
position = 'bottom',
|
|
21
40
|
align = 'center',
|
|
22
41
|
style,
|
|
23
|
-
showArrow = false,
|
|
24
|
-
arrowSize = DEFAULT_ARROW_SIZE,
|
|
25
42
|
inverted = false,
|
|
26
43
|
// @ts-ignore
|
|
27
44
|
dataSet,
|
|
28
|
-
withBackdropDismiss = false,
|
|
29
45
|
offset = 8,
|
|
30
|
-
|
|
46
|
+
horizontalOffset = 0,
|
|
31
47
|
triggerDimensions,
|
|
48
|
+
overlay,
|
|
32
49
|
...rest
|
|
33
|
-
}:
|
|
50
|
+
}: PopoverPanelProps) => {
|
|
34
51
|
const {
|
|
35
52
|
popoverLayoutRef,
|
|
36
53
|
targetLayoutRef,
|
|
@@ -38,111 +55,31 @@ const Popover = ({
|
|
|
38
55
|
calculatedPosition,
|
|
39
56
|
calculateAndSetPosition,
|
|
40
57
|
handlePopoverLayout,
|
|
41
|
-
} = usePopover({
|
|
42
|
-
isOpen,
|
|
43
|
-
position,
|
|
44
|
-
align,
|
|
45
|
-
showArrow,
|
|
46
|
-
arrowSize,
|
|
47
|
-
offset,
|
|
48
|
-
});
|
|
58
|
+
} = usePopover({ isOpen, position, align, offset, horizontalOffset });
|
|
49
59
|
|
|
50
60
|
const popoverRef = useRef<View>(null);
|
|
51
61
|
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (width !== 0 || height !== 0) {
|
|
57
|
-
const newLayout = { x, y, width, height };
|
|
58
|
-
const changed =
|
|
59
|
-
!targetLayoutRef.current ||
|
|
60
|
-
targetLayoutRef.current.x !== newLayout.x ||
|
|
61
|
-
targetLayoutRef.current.y !== newLayout.y ||
|
|
62
|
-
targetLayoutRef.current.width !== newLayout.width ||
|
|
63
|
-
targetLayoutRef.current.height !== newLayout.height;
|
|
64
|
-
|
|
65
|
-
if (changed) {
|
|
66
|
-
targetLayoutRef.current = newLayout;
|
|
67
|
-
calculateAndSetPosition();
|
|
68
|
-
}
|
|
69
|
-
} else {
|
|
70
|
-
targetLayoutRef.current = null;
|
|
71
|
-
calculateAndSetPosition();
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
);
|
|
75
|
-
} else {
|
|
76
|
-
targetLayoutRef.current = null;
|
|
77
|
-
calculateAndSetPosition();
|
|
78
|
-
}
|
|
79
|
-
}, [triggerRef, calculateAndSetPosition, targetLayoutRef]);
|
|
80
|
-
|
|
81
|
-
useLayoutEffect(() => {
|
|
82
|
-
if (isOpen) {
|
|
83
|
-
const timeoutId = setTimeout(measureTarget, 0);
|
|
84
|
-
return () => clearTimeout(timeoutId);
|
|
85
|
-
}
|
|
86
|
-
return;
|
|
87
|
-
}, [isOpen, measureTarget, triggerDimensions]);
|
|
88
|
-
|
|
89
|
-
useLayoutEffect(() => {
|
|
90
|
-
if (!isOpen) return;
|
|
91
|
-
const handleResize = () => {
|
|
92
|
-
if (triggerRef.current && isOpen) {
|
|
93
|
-
window.requestAnimationFrame(measureTarget);
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
window.addEventListener('resize', handleResize);
|
|
97
|
-
window.addEventListener('scroll', handleResize, true);
|
|
98
|
-
return () => {
|
|
99
|
-
window.removeEventListener('resize', handleResize);
|
|
100
|
-
window.removeEventListener('scroll', handleResize, true);
|
|
101
|
-
};
|
|
102
|
-
}, [isOpen, measureTarget, triggerRef]);
|
|
103
|
-
|
|
104
|
-
useEffect(() => {
|
|
105
|
-
if (!isOpen || !onClose || withBackdropDismiss) return;
|
|
106
|
-
const handleClickOutside = (event: MouseEvent) => {
|
|
107
|
-
const popoverElement = popoverRef.current as any as HTMLElement;
|
|
108
|
-
const targetElement = triggerRef.current as any as HTMLElement;
|
|
109
|
-
if (
|
|
110
|
-
popoverElement &&
|
|
111
|
-
!popoverElement.contains(event.target as Node) &&
|
|
112
|
-
targetElement &&
|
|
113
|
-
!targetElement.contains(event.target as Node)
|
|
114
|
-
) {
|
|
115
|
-
onClose();
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
document.addEventListener('mousedown', handleClickOutside, { capture: true });
|
|
119
|
-
return () => {
|
|
120
|
-
document.removeEventListener('mousedown', handleClickOutside, { capture: true });
|
|
121
|
-
};
|
|
122
|
-
}, [isOpen, onClose, popoverRef, triggerRef, withBackdropDismiss]);
|
|
123
|
-
|
|
124
|
-
const arrowStyles = useArrowStyles({
|
|
125
|
-
showArrow,
|
|
126
|
-
arrowSize,
|
|
127
|
-
style,
|
|
62
|
+
const { popoverStyle } = usePlatformMeasure({
|
|
63
|
+
triggerRef,
|
|
64
|
+
isOpen,
|
|
65
|
+
onClose,
|
|
128
66
|
calculatedPosition,
|
|
67
|
+
calculateAndSetPosition,
|
|
129
68
|
targetLayoutRef,
|
|
130
|
-
|
|
131
|
-
|
|
69
|
+
popoverRef,
|
|
70
|
+
triggerDimensions,
|
|
132
71
|
});
|
|
133
72
|
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
};
|
|
145
|
-
}, [calculatedPosition]);
|
|
73
|
+
const panelContextValue = useMemo<PopoverPanelContextValue>(
|
|
74
|
+
() => ({
|
|
75
|
+
calculatedPosition,
|
|
76
|
+
targetLayoutRef,
|
|
77
|
+
popoverLayoutRef,
|
|
78
|
+
actualPositionRef,
|
|
79
|
+
containerStyle: style,
|
|
80
|
+
}),
|
|
81
|
+
[calculatedPosition, targetLayoutRef, popoverLayoutRef, actualPositionRef, style],
|
|
82
|
+
);
|
|
146
83
|
|
|
147
84
|
const Wrapper = inverted ? ScopedTheme : Fragment;
|
|
148
85
|
const WrapperProps = inverted
|
|
@@ -155,22 +92,84 @@ const Popover = ({
|
|
|
155
92
|
|
|
156
93
|
return (
|
|
157
94
|
<Portal>
|
|
158
|
-
<
|
|
159
|
-
{
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
</Wrapper>
|
|
95
|
+
<PopoverPanelContext value={panelContextValue}>
|
|
96
|
+
<Wrapper {...(WrapperProps as any)}>
|
|
97
|
+
{overlay}
|
|
98
|
+
<View
|
|
99
|
+
onLayout={handlePopoverLayout}
|
|
100
|
+
style={[popoverStyles.popoverContainer, style, popoverStyle]}
|
|
101
|
+
{...{ dataSet }}
|
|
102
|
+
{...rest}
|
|
103
|
+
ref={popoverRef}>
|
|
104
|
+
{children}
|
|
105
|
+
</View>
|
|
106
|
+
</Wrapper>
|
|
107
|
+
</PopoverPanelContext>
|
|
172
108
|
</Portal>
|
|
173
109
|
);
|
|
174
110
|
};
|
|
175
111
|
|
|
176
|
-
|
|
112
|
+
type PopoverTriggerProps = {
|
|
113
|
+
children: ReactElement;
|
|
114
|
+
ref?: Ref<any>;
|
|
115
|
+
triggerRef?: Ref<any>;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export const PopoverTrigger = memo(
|
|
119
|
+
({ children, ref: refProp, triggerRef: triggerRefProp }: PopoverTriggerProps) => {
|
|
120
|
+
const { triggerRef } = useContext(PopoverContext);
|
|
121
|
+
const mergedRef = useMemo(
|
|
122
|
+
() => mergeRefs([triggerRef, refProp, triggerRefProp]),
|
|
123
|
+
[triggerRef, refProp, triggerRefProp],
|
|
124
|
+
);
|
|
125
|
+
return cloneElement(children as ReactElement<{ ref?: unknown }>, { ref: mergedRef });
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
PopoverTrigger.displayName = 'Popover_Trigger';
|
|
129
|
+
|
|
130
|
+
export const PopoverContent = memo(({ children }: { children?: ReactNode }) => <>{children}</>);
|
|
131
|
+
PopoverContent.displayName = 'Popover_Content';
|
|
132
|
+
|
|
133
|
+
export const PopoverOverlay = memo(({ style, onPress, ...rest }: PressableProps) => {
|
|
134
|
+
const { isOpen, onClose } = useContext(PopoverContext);
|
|
135
|
+
if (!isOpen) return null;
|
|
136
|
+
return (
|
|
137
|
+
<Pressable
|
|
138
|
+
{...rest}
|
|
139
|
+
onPress={onPress ?? onClose}
|
|
140
|
+
style={[popoverStyles.overlay, style as StyleProp<ViewStyle>]}
|
|
141
|
+
/>
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
PopoverOverlay.displayName = 'Popover_Overlay';
|
|
145
|
+
|
|
146
|
+
type PopoverArrowProps = {
|
|
147
|
+
size?: number;
|
|
148
|
+
style?: StyleProp<ViewStyle>;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const PopoverArrow = memo(({ size = DEFAULT_ARROW_SIZE, style }: PopoverArrowProps) => {
|
|
152
|
+
const {
|
|
153
|
+
calculatedPosition,
|
|
154
|
+
targetLayoutRef,
|
|
155
|
+
popoverLayoutRef,
|
|
156
|
+
actualPositionRef,
|
|
157
|
+
containerStyle,
|
|
158
|
+
} = useContext(PopoverPanelContext);
|
|
159
|
+
|
|
160
|
+
const arrowStyles = useArrowStyles({
|
|
161
|
+
arrowSize: size,
|
|
162
|
+
containerStyle,
|
|
163
|
+
calculatedPosition,
|
|
164
|
+
targetLayoutRef,
|
|
165
|
+
popoverLayoutRef,
|
|
166
|
+
actualPositionRef,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (!arrowStyles || Object.keys(arrowStyles).length === 0) return null;
|
|
170
|
+
return <View style={[arrowStyles, style]} />;
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
PopoverArrow.displayName = 'Popover_Arrow';
|
|
174
|
+
|
|
175
|
+
export default memo(createPopoverRoot(PopoverPanel));
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ComponentType,
|
|
3
|
+
memo,
|
|
4
|
+
type ReactElement,
|
|
5
|
+
type ReactNode,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import { type View } from 'react-native';
|
|
10
|
+
|
|
11
|
+
import { extractSubcomponents } from '../../utils/extractSubcomponents';
|
|
12
|
+
import { PopoverContext, type PopoverProps } from './common';
|
|
13
|
+
|
|
14
|
+
type PopoverPanelInternalProps = PopoverProps & { overlay?: ReactNode };
|
|
15
|
+
|
|
16
|
+
export const createPopoverRoot = (PopoverPanel: ComponentType<PopoverPanelInternalProps>) => {
|
|
17
|
+
const PopoverRoot = ({
|
|
18
|
+
triggerRef: triggerRefProp,
|
|
19
|
+
isOpen,
|
|
20
|
+
onClose,
|
|
21
|
+
children,
|
|
22
|
+
...rest
|
|
23
|
+
}: PopoverProps) => {
|
|
24
|
+
const internalTriggerRef = useRef<View>(null);
|
|
25
|
+
|
|
26
|
+
const {
|
|
27
|
+
Popover_Trigger,
|
|
28
|
+
Popover_Content,
|
|
29
|
+
Popover_Overlay,
|
|
30
|
+
rest: restChildren,
|
|
31
|
+
} = extractSubcomponents({
|
|
32
|
+
children,
|
|
33
|
+
allowedChildren: [
|
|
34
|
+
{ name: 'Popover_Trigger', allowMultiple: false },
|
|
35
|
+
{ name: 'Popover_Content', allowMultiple: false },
|
|
36
|
+
{ name: 'Popover_Overlay', allowMultiple: false },
|
|
37
|
+
] as const,
|
|
38
|
+
includeRest: true,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const hasTrigger = Popover_Trigger.length > 0;
|
|
42
|
+
const resolvedTriggerRef = triggerRefProp ?? (hasTrigger ? internalTriggerRef : undefined);
|
|
43
|
+
|
|
44
|
+
const panelContent =
|
|
45
|
+
Popover_Content.length > 0
|
|
46
|
+
? (Popover_Content[0] as ReactElement<{ children?: ReactNode }>).props.children
|
|
47
|
+
: restChildren;
|
|
48
|
+
|
|
49
|
+
const contextValue = useMemo(
|
|
50
|
+
() => ({
|
|
51
|
+
triggerRef: resolvedTriggerRef ?? internalTriggerRef,
|
|
52
|
+
isOpen,
|
|
53
|
+
onClose,
|
|
54
|
+
}),
|
|
55
|
+
[resolvedTriggerRef, isOpen, onClose],
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<PopoverContext value={contextValue}>
|
|
60
|
+
{hasTrigger && Popover_Trigger[0]}
|
|
61
|
+
<PopoverPanel
|
|
62
|
+
triggerRef={resolvedTriggerRef}
|
|
63
|
+
isOpen={isOpen}
|
|
64
|
+
onClose={onClose}
|
|
65
|
+
overlay={Popover_Overlay[0]}
|
|
66
|
+
{...rest}>
|
|
67
|
+
{panelContent}
|
|
68
|
+
</PopoverPanel>
|
|
69
|
+
</PopoverContext>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return memo(PopoverRoot);
|
|
74
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ReactNode, RefObject } from 'react';
|
|
2
|
+
import { createContext } from 'react';
|
|
2
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
4
|
import type { LayoutRectangle, StyleProp, View, ViewStyle } from 'react-native';
|
|
4
5
|
import { Dimensions, StyleSheet } from 'react-native';
|
|
@@ -15,10 +16,36 @@ export const popoverDefaultStyles = {
|
|
|
15
16
|
opacity: 0,
|
|
16
17
|
};
|
|
17
18
|
|
|
19
|
+
export type PopoverContextValue = {
|
|
20
|
+
triggerRef: RefObject<View | any>;
|
|
21
|
+
isOpen: boolean;
|
|
22
|
+
onClose?: () => void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const PopoverContext = createContext<PopoverContextValue>({
|
|
26
|
+
isOpen: false,
|
|
27
|
+
triggerRef: { current: null },
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export type PopoverPanelContextValue = {
|
|
31
|
+
calculatedPosition: ViewStyle | null;
|
|
32
|
+
targetLayoutRef: RefObject<LayoutRectangle | null>;
|
|
33
|
+
popoverLayoutRef: RefObject<LayoutRectangle | null>;
|
|
34
|
+
actualPositionRef: RefObject<Position | undefined>;
|
|
35
|
+
containerStyle?: StyleProp<ViewStyle>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const PopoverPanelContext = createContext<PopoverPanelContextValue>({
|
|
39
|
+
calculatedPosition: null,
|
|
40
|
+
targetLayoutRef: { current: null },
|
|
41
|
+
popoverLayoutRef: { current: null },
|
|
42
|
+
actualPositionRef: { current: undefined },
|
|
43
|
+
});
|
|
44
|
+
|
|
18
45
|
export type PopoverProps = {
|
|
19
46
|
inverted?: boolean;
|
|
20
47
|
/** Reference to the element the popover should anchor to */
|
|
21
|
-
triggerRef
|
|
48
|
+
triggerRef?: RefObject<View | any>;
|
|
22
49
|
/** Content to display inside the popover */
|
|
23
50
|
children: ReactNode;
|
|
24
51
|
/** Whether the popover is visible */
|
|
@@ -31,13 +58,10 @@ export type PopoverProps = {
|
|
|
31
58
|
align?: Align;
|
|
32
59
|
/** Optional style for the popover container */
|
|
33
60
|
style?: StyleProp<ViewStyle>;
|
|
34
|
-
/**
|
|
35
|
-
showArrow?: boolean;
|
|
36
|
-
/** Size of the arrow */
|
|
37
|
-
arrowSize?: number;
|
|
38
|
-
withBackdropDismiss?: boolean;
|
|
61
|
+
/** Gap between the popover and the trigger along the main axis */
|
|
39
62
|
offset?: number;
|
|
40
|
-
|
|
63
|
+
/** Additional horizontal shift applied to the popover (positive = right) */
|
|
64
|
+
horizontalOffset?: number;
|
|
41
65
|
/** Optional trigger dimensions to trigger re-measurement when changed */
|
|
42
66
|
triggerDimensions?: { width: number; height: number } | null;
|
|
43
67
|
};
|
|
@@ -165,13 +189,12 @@ export const adjustPositionForBoundaries = (
|
|
|
165
189
|
// --- Arrow Style Hook ---
|
|
166
190
|
|
|
167
191
|
interface UseArrowStylesProps {
|
|
168
|
-
showArrow?: boolean;
|
|
169
192
|
arrowSize: number;
|
|
170
|
-
|
|
193
|
+
containerStyle?: StyleProp<ViewStyle>;
|
|
171
194
|
calculatedPosition: ViewStyle | null;
|
|
172
195
|
targetLayoutRef: RefObject<LayoutRectangle | null>;
|
|
173
196
|
popoverLayoutRef: RefObject<LayoutRectangle | null>;
|
|
174
|
-
actualPositionRef: RefObject<Position | undefined>;
|
|
197
|
+
actualPositionRef: RefObject<Position | undefined>;
|
|
175
198
|
}
|
|
176
199
|
|
|
177
200
|
// Define a base style for the popover container to extract default background
|
|
@@ -183,9 +206,8 @@ const basePopoverStyle = StyleSheet.create({
|
|
|
183
206
|
});
|
|
184
207
|
|
|
185
208
|
export const useArrowStyles = ({
|
|
186
|
-
showArrow,
|
|
187
209
|
arrowSize,
|
|
188
|
-
|
|
210
|
+
containerStyle,
|
|
189
211
|
calculatedPosition,
|
|
190
212
|
targetLayoutRef,
|
|
191
213
|
popoverLayoutRef,
|
|
@@ -193,7 +215,6 @@ export const useArrowStyles = ({
|
|
|
193
215
|
}: UseArrowStylesProps): ViewStyle => {
|
|
194
216
|
return useMemo(() => {
|
|
195
217
|
if (
|
|
196
|
-
!showArrow ||
|
|
197
218
|
!targetLayoutRef.current ||
|
|
198
219
|
!popoverLayoutRef.current ||
|
|
199
220
|
!calculatedPosition ||
|
|
@@ -213,7 +234,7 @@ export const useArrowStyles = ({
|
|
|
213
234
|
} = targetLayoutRef.current;
|
|
214
235
|
|
|
215
236
|
const arrowHalfSize = arrowSize / 2;
|
|
216
|
-
const popoverStyleFlat = StyleSheet.flatten(
|
|
237
|
+
const popoverStyleFlat = StyleSheet.flatten(containerStyle || {});
|
|
217
238
|
const containerStyleFlat = StyleSheet.flatten(basePopoverStyle.container);
|
|
218
239
|
|
|
219
240
|
const backgroundColor =
|
|
@@ -320,14 +341,11 @@ export const useArrowStyles = ({
|
|
|
320
341
|
default:
|
|
321
342
|
return {};
|
|
322
343
|
}
|
|
323
|
-
// Use refs directly in dependency array for useMemo
|
|
324
|
-
// React checks ref.current internally when deciding memoization
|
|
325
344
|
}, [
|
|
326
|
-
showArrow,
|
|
327
345
|
arrowSize,
|
|
328
|
-
|
|
346
|
+
containerStyle,
|
|
329
347
|
calculatedPosition,
|
|
330
|
-
targetLayoutRef,
|
|
348
|
+
targetLayoutRef,
|
|
331
349
|
popoverLayoutRef,
|
|
332
350
|
actualPositionRef,
|
|
333
351
|
]);
|
|
@@ -339,18 +357,16 @@ interface UsePopoverProps {
|
|
|
339
357
|
isOpen: boolean;
|
|
340
358
|
position: Position | undefined;
|
|
341
359
|
align: Align | undefined;
|
|
342
|
-
showArrow: boolean | undefined;
|
|
343
|
-
arrowSize: number;
|
|
344
360
|
offset?: number;
|
|
361
|
+
horizontalOffset?: number;
|
|
345
362
|
}
|
|
346
363
|
|
|
347
364
|
export const usePopover = ({
|
|
348
365
|
isOpen,
|
|
349
366
|
position = 'bottom',
|
|
350
367
|
align = 'center',
|
|
351
|
-
showArrow = true,
|
|
352
|
-
arrowSize = DEFAULT_ARROW_SIZE,
|
|
353
368
|
offset = 0,
|
|
369
|
+
horizontalOffset = 0,
|
|
354
370
|
}: UsePopoverProps) => {
|
|
355
371
|
const popoverLayoutRef = useRef<LayoutRectangle | null>(null);
|
|
356
372
|
const targetLayoutRef = useRef<LayoutRectangle | null>(null);
|
|
@@ -359,22 +375,20 @@ export const usePopover = ({
|
|
|
359
375
|
|
|
360
376
|
const calculateAndSetPosition = useCallback(() => {
|
|
361
377
|
if (!targetLayoutRef.current || !popoverLayoutRef.current) {
|
|
362
|
-
setCalculatedPosition(popoverDefaultStyles);
|
|
378
|
+
setCalculatedPosition(popoverDefaultStyles);
|
|
363
379
|
return;
|
|
364
380
|
}
|
|
365
381
|
|
|
366
382
|
const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
|
|
367
|
-
const effectiveArrowSize = showArrow ? arrowSize : 0;
|
|
368
383
|
|
|
369
384
|
let { top: initialTop, left: initialLeft } = getInitialPosition(
|
|
370
385
|
position,
|
|
371
386
|
align,
|
|
372
387
|
targetLayoutRef.current,
|
|
373
388
|
popoverLayoutRef.current,
|
|
374
|
-
|
|
389
|
+
0,
|
|
375
390
|
);
|
|
376
391
|
|
|
377
|
-
// Apply offset based on the initial intended position
|
|
378
392
|
switch (position) {
|
|
379
393
|
case 'top':
|
|
380
394
|
initialTop -= offset;
|
|
@@ -390,18 +404,20 @@ export const usePopover = ({
|
|
|
390
404
|
break;
|
|
391
405
|
}
|
|
392
406
|
|
|
407
|
+
initialLeft += horizontalOffset;
|
|
408
|
+
|
|
393
409
|
const { finalTop, finalLeft, finalPosition } = adjustPositionForBoundaries(
|
|
394
|
-
position,
|
|
395
|
-
initialTop,
|
|
396
|
-
initialLeft,
|
|
410
|
+
position,
|
|
411
|
+
initialTop,
|
|
412
|
+
initialLeft,
|
|
397
413
|
targetLayoutRef.current,
|
|
398
414
|
popoverLayoutRef.current,
|
|
399
415
|
screenHeight,
|
|
400
416
|
screenWidth,
|
|
401
|
-
|
|
417
|
+
0,
|
|
402
418
|
);
|
|
403
419
|
|
|
404
|
-
actualPositionRef.current = finalPosition;
|
|
420
|
+
actualPositionRef.current = finalPosition;
|
|
405
421
|
|
|
406
422
|
setCalculatedPosition({
|
|
407
423
|
position: 'absolute',
|
|
@@ -409,7 +425,7 @@ export const usePopover = ({
|
|
|
409
425
|
left: finalLeft,
|
|
410
426
|
opacity: 1,
|
|
411
427
|
});
|
|
412
|
-
}, [position, align,
|
|
428
|
+
}, [position, align, offset, horizontalOffset]);
|
|
413
429
|
|
|
414
430
|
const handlePopoverLayout = useCallback(
|
|
415
431
|
(event: { nativeEvent: { layout: LayoutRectangle } }) => {
|
|
@@ -435,7 +451,7 @@ export const usePopover = ({
|
|
|
435
451
|
calculateAndSetPosition();
|
|
436
452
|
}
|
|
437
453
|
// This effect specifically handles prop changes
|
|
438
|
-
}, [isOpen, position, align,
|
|
454
|
+
}, [isOpen, position, align, calculateAndSetPosition]);
|
|
439
455
|
|
|
440
456
|
// Effect to reset layout refs when popover is closed
|
|
441
457
|
useEffect(() => {
|
|
@@ -1,2 +1,19 @@
|
|
|
1
|
+
import { getRegisteredComponentWithFallback } from '../../core';
|
|
2
|
+
import PopoverDefault, {
|
|
3
|
+
PopoverArrow,
|
|
4
|
+
PopoverContent,
|
|
5
|
+
PopoverOverlay,
|
|
6
|
+
PopoverTrigger,
|
|
7
|
+
} from './Popover';
|
|
8
|
+
|
|
9
|
+
const PopoverBase = getRegisteredComponentWithFallback('Popover', PopoverDefault);
|
|
10
|
+
|
|
11
|
+
export const Popover = Object.assign(PopoverBase, {
|
|
12
|
+
Trigger: PopoverTrigger,
|
|
13
|
+
Content: PopoverContent,
|
|
14
|
+
Arrow: PopoverArrow,
|
|
15
|
+
Overlay: PopoverOverlay,
|
|
16
|
+
});
|
|
17
|
+
|
|
1
18
|
export type { Align, PopoverProps, Position } from './common';
|
|
2
|
-
export {
|
|
19
|
+
export { PopoverContext, PopoverPanelContext } from './common';
|
|
@@ -1,52 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { AppState, Dimensions, Platform
|
|
3
|
-
import { ScopedTheme, UnistylesRuntime } from 'react-native-unistyles';
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect } from 'react';
|
|
2
|
+
import { AppState, Dimensions, Platform } from 'react-native';
|
|
4
3
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
DEFAULT_ARROW_SIZE,
|
|
8
|
-
popoverDefaultStyles,
|
|
9
|
-
type PopoverProps,
|
|
10
|
-
useArrowStyles,
|
|
11
|
-
usePopover,
|
|
12
|
-
} from './common';
|
|
13
|
-
import { popoverStyles } from './utils';
|
|
4
|
+
import { popoverDefaultStyles } from './common';
|
|
5
|
+
import type { UsePlatformMeasureArgs, UsePlatformMeasureResult } from './usePlatformMeasure';
|
|
14
6
|
|
|
15
|
-
const
|
|
7
|
+
export const usePlatformMeasure = ({
|
|
16
8
|
triggerRef,
|
|
17
|
-
children,
|
|
18
9
|
isOpen,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
style,
|
|
23
|
-
showArrow = false,
|
|
24
|
-
arrowSize = DEFAULT_ARROW_SIZE,
|
|
25
|
-
inverted = false,
|
|
10
|
+
calculatedPosition,
|
|
11
|
+
calculateAndSetPosition,
|
|
12
|
+
targetLayoutRef,
|
|
26
13
|
triggerDimensions,
|
|
27
|
-
|
|
28
|
-
...rest
|
|
29
|
-
}: PopoverProps) => {
|
|
30
|
-
const {
|
|
31
|
-
popoverLayoutRef,
|
|
32
|
-
targetLayoutRef,
|
|
33
|
-
actualPositionRef,
|
|
34
|
-
calculatedPosition,
|
|
35
|
-
calculateAndSetPosition,
|
|
36
|
-
handlePopoverLayout,
|
|
37
|
-
} = usePopover({
|
|
38
|
-
isOpen,
|
|
39
|
-
position,
|
|
40
|
-
align,
|
|
41
|
-
showArrow,
|
|
42
|
-
arrowSize,
|
|
43
|
-
offset,
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
const popoverRef = useRef<View>(null);
|
|
47
|
-
|
|
14
|
+
}: UsePlatformMeasureArgs): UsePlatformMeasureResult => {
|
|
48
15
|
const measureTarget = useCallback(() => {
|
|
49
|
-
if (triggerRef
|
|
16
|
+
if (triggerRef?.current) {
|
|
50
17
|
triggerRef.current.measure(
|
|
51
18
|
(
|
|
52
19
|
_fx: number,
|
|
@@ -117,48 +84,7 @@ const Popover = ({
|
|
|
117
84
|
};
|
|
118
85
|
}, [isOpen, measureTarget]);
|
|
119
86
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
arrowSize,
|
|
123
|
-
style,
|
|
124
|
-
calculatedPosition,
|
|
125
|
-
targetLayoutRef,
|
|
126
|
-
popoverLayoutRef,
|
|
127
|
-
actualPositionRef,
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
const popoverStyle = calculatedPosition ?? popoverDefaultStyles;
|
|
131
|
-
const Wrapper = inverted ? ScopedTheme : Fragment;
|
|
132
|
-
|
|
133
|
-
if (!isOpen && popoverStyle.opacity === 0) {
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const handleOutsidePress = () => {
|
|
138
|
-
if (isOpen && onClose) {
|
|
139
|
-
onClose();
|
|
140
|
-
}
|
|
87
|
+
return {
|
|
88
|
+
popoverStyle: (calculatedPosition ?? popoverDefaultStyles) as any,
|
|
141
89
|
};
|
|
142
|
-
|
|
143
|
-
return (
|
|
144
|
-
<Portal>
|
|
145
|
-
<Wrapper
|
|
146
|
-
{...(inverted
|
|
147
|
-
? { name: UnistylesRuntime.themeName === 'dark' ? 'light' : 'dark' }
|
|
148
|
-
: ({} as { name: 'light' }))}>
|
|
149
|
-
<Pressable onPress={handleOutsidePress} style={popoverStyles.overlay} />
|
|
150
|
-
|
|
151
|
-
<View
|
|
152
|
-
ref={popoverRef}
|
|
153
|
-
onLayout={handlePopoverLayout}
|
|
154
|
-
style={[popoverStyles.popoverContainer, style, popoverStyle]}
|
|
155
|
-
{...rest}>
|
|
156
|
-
{children}
|
|
157
|
-
{showArrow && popoverStyle.opacity === 1 && <View style={arrowStyles} />}
|
|
158
|
-
</View>
|
|
159
|
-
</Wrapper>
|
|
160
|
-
</Portal>
|
|
161
|
-
);
|
|
162
90
|
};
|
|
163
|
-
|
|
164
|
-
export default memo(Popover);
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { type RefObject, useCallback, useEffect, useLayoutEffect, useMemo } from 'react';
|
|
2
|
+
import type { LayoutRectangle, View, ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { popoverDefaultStyles } from './common';
|
|
5
|
+
|
|
6
|
+
export type UsePlatformMeasureArgs = {
|
|
7
|
+
triggerRef: RefObject<View | any> | undefined;
|
|
8
|
+
isOpen: boolean;
|
|
9
|
+
onClose?: () => void;
|
|
10
|
+
calculatedPosition: ViewStyle | null;
|
|
11
|
+
calculateAndSetPosition: () => void;
|
|
12
|
+
targetLayoutRef: RefObject<LayoutRectangle | null>;
|
|
13
|
+
popoverRef: RefObject<View | null>;
|
|
14
|
+
triggerDimensions?: { width: number; height: number } | null;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type UsePlatformMeasureResult = {
|
|
18
|
+
/** Platform-adjusted popover position (includes scroll offset on web) */
|
|
19
|
+
popoverStyle: ViewStyle;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const usePlatformMeasure = ({
|
|
23
|
+
triggerRef,
|
|
24
|
+
isOpen,
|
|
25
|
+
onClose,
|
|
26
|
+
calculatedPosition,
|
|
27
|
+
calculateAndSetPosition,
|
|
28
|
+
targetLayoutRef,
|
|
29
|
+
popoverRef,
|
|
30
|
+
triggerDimensions,
|
|
31
|
+
}: UsePlatformMeasureArgs): UsePlatformMeasureResult => {
|
|
32
|
+
const measureTarget = useCallback(() => {
|
|
33
|
+
if (triggerRef?.current) {
|
|
34
|
+
triggerRef.current.measureInWindow(
|
|
35
|
+
(x: number, y: number, width: number, height: number) => {
|
|
36
|
+
if (width !== 0 || height !== 0) {
|
|
37
|
+
const newLayout = { x, y, width, height };
|
|
38
|
+
const changed =
|
|
39
|
+
!targetLayoutRef.current ||
|
|
40
|
+
targetLayoutRef.current.x !== newLayout.x ||
|
|
41
|
+
targetLayoutRef.current.y !== newLayout.y ||
|
|
42
|
+
targetLayoutRef.current.width !== newLayout.width ||
|
|
43
|
+
targetLayoutRef.current.height !== newLayout.height;
|
|
44
|
+
|
|
45
|
+
if (changed) {
|
|
46
|
+
targetLayoutRef.current = newLayout;
|
|
47
|
+
calculateAndSetPosition();
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
targetLayoutRef.current = null;
|
|
51
|
+
calculateAndSetPosition();
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
} else {
|
|
56
|
+
targetLayoutRef.current = null;
|
|
57
|
+
calculateAndSetPosition();
|
|
58
|
+
}
|
|
59
|
+
}, [triggerRef, calculateAndSetPosition, targetLayoutRef]);
|
|
60
|
+
|
|
61
|
+
useLayoutEffect(() => {
|
|
62
|
+
if (isOpen) {
|
|
63
|
+
const timeoutId = setTimeout(measureTarget, 0);
|
|
64
|
+
return () => clearTimeout(timeoutId);
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}, [isOpen, measureTarget, triggerDimensions]);
|
|
68
|
+
|
|
69
|
+
useLayoutEffect(() => {
|
|
70
|
+
if (!isOpen) return;
|
|
71
|
+
const handleResize = () => {
|
|
72
|
+
if (triggerRef?.current && isOpen) {
|
|
73
|
+
window.requestAnimationFrame(measureTarget);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
window.addEventListener('resize', handleResize);
|
|
77
|
+
window.addEventListener('scroll', handleResize, true);
|
|
78
|
+
return () => {
|
|
79
|
+
window.removeEventListener('resize', handleResize);
|
|
80
|
+
window.removeEventListener('scroll', handleResize, true);
|
|
81
|
+
};
|
|
82
|
+
}, [isOpen, measureTarget, triggerRef]);
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (!isOpen || !onClose) return;
|
|
86
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
87
|
+
const popoverElement = popoverRef.current as any as HTMLElement;
|
|
88
|
+
const targetElement = triggerRef?.current as any as HTMLElement;
|
|
89
|
+
if (
|
|
90
|
+
popoverElement &&
|
|
91
|
+
!popoverElement.contains(event.target as Node) &&
|
|
92
|
+
targetElement &&
|
|
93
|
+
!targetElement.contains(event.target as Node)
|
|
94
|
+
) {
|
|
95
|
+
onClose();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
document.addEventListener('mousedown', handleClickOutside, { capture: true });
|
|
99
|
+
return () => {
|
|
100
|
+
document.removeEventListener('mousedown', handleClickOutside, { capture: true });
|
|
101
|
+
};
|
|
102
|
+
}, [isOpen, onClose, popoverRef, triggerRef]);
|
|
103
|
+
|
|
104
|
+
const popoverStyle = useMemo(() => {
|
|
105
|
+
if (!calculatedPosition) return popoverDefaultStyles;
|
|
106
|
+
|
|
107
|
+
const scrollX = window.scrollX ?? window.pageXOffset ?? 0;
|
|
108
|
+
const scrollY = window.scrollY ?? window.pageYOffset ?? 0;
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
...calculatedPosition,
|
|
112
|
+
left: (calculatedPosition.left as number) + scrollX,
|
|
113
|
+
top: (calculatedPosition.top as number) + scrollY,
|
|
114
|
+
};
|
|
115
|
+
}, [calculatedPosition]);
|
|
116
|
+
|
|
117
|
+
return { popoverStyle };
|
|
118
|
+
};
|
|
@@ -15,24 +15,17 @@ const popoverStylesDefault = StyleSheet.create(theme => ({
|
|
|
15
15
|
elevation: 5,
|
|
16
16
|
zIndex: 100,
|
|
17
17
|
},
|
|
18
|
-
|
|
18
|
+
overlay: {
|
|
19
19
|
position: 'absolute',
|
|
20
20
|
top: 0,
|
|
21
21
|
left: 0,
|
|
22
22
|
right: 0,
|
|
23
23
|
bottom: 0,
|
|
24
|
+
backgroundColor: 'transparent',
|
|
24
25
|
_web: {
|
|
25
26
|
cursor: 'default',
|
|
26
27
|
},
|
|
27
28
|
},
|
|
28
|
-
overlay: {
|
|
29
|
-
position: 'absolute',
|
|
30
|
-
top: 0,
|
|
31
|
-
bottom: 0,
|
|
32
|
-
left: 0,
|
|
33
|
-
right: 0,
|
|
34
|
-
backgroundColor: 'transparent',
|
|
35
|
-
},
|
|
36
29
|
}));
|
|
37
30
|
|
|
38
31
|
export const popoverStyles = getRegisteredComponentStylesWithFallback(
|