@idealyst/components 1.2.136 → 1.2.138
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/package.json +4 -4
- package/src/Button/Button.web.tsx +0 -2
- package/src/Dialog/Dialog.native.tsx +69 -41
- package/src/IconButton/IconButton.web.tsx +0 -2
- package/src/Pressable/Pressable.web.tsx +0 -6
- package/src/examples/MenuExamples.tsx +59 -2
- package/src/internal/PositionedPortal.tsx +32 -30
- package/src/utils/events/events.native.ts +8 -4
- package/src/utils/events/events.web.ts +16 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idealyst/components",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.138",
|
|
4
4
|
"description": "Shared component library for React and React Native",
|
|
5
5
|
"documentation": "https://github.com/IdealystIO/idealyst-framework/tree/main/packages/components#readme",
|
|
6
6
|
"readme": "README.md",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"publish:npm": "npm publish"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@idealyst/theme": "^1.2.
|
|
59
|
+
"@idealyst/theme": "^1.2.138",
|
|
60
60
|
"@mdi/js": ">=7.0.0",
|
|
61
61
|
"@mdi/react": ">=1.0.0",
|
|
62
62
|
"@react-native-vector-icons/common": ">=12.0.0",
|
|
@@ -111,8 +111,8 @@
|
|
|
111
111
|
},
|
|
112
112
|
"devDependencies": {
|
|
113
113
|
"@idealyst/blur": "^1.2.40",
|
|
114
|
-
"@idealyst/theme": "^1.2.
|
|
115
|
-
"@idealyst/tooling": "^1.2.
|
|
114
|
+
"@idealyst/theme": "^1.2.138",
|
|
115
|
+
"@idealyst/tooling": "^1.2.138",
|
|
116
116
|
"@mdi/react": "^1.6.1",
|
|
117
117
|
"@types/react": "^19.1.0",
|
|
118
118
|
"react": "^19.1.0",
|
|
@@ -65,8 +65,6 @@ const Button = forwardRef<IdealystElement, ButtonProps>((props, ref) => {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
68
|
-
e.preventDefault();
|
|
69
|
-
e.stopPropagation();
|
|
70
68
|
if (!isDisabled && pressHandler) {
|
|
71
69
|
pressHandler(createPressEvent(e));
|
|
72
70
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { useEffect, forwardRef, useMemo
|
|
1
|
+
import { useEffect, forwardRef, useMemo } from 'react';
|
|
2
2
|
import { Modal, View, Text, TouchableOpacity, TouchableWithoutFeedback, BackHandler, Platform, Keyboard, useWindowDimensions } from 'react-native';
|
|
3
|
-
import Animated, { useSharedValue, useAnimatedStyle, withTiming, Easing } from 'react-native-reanimated';
|
|
3
|
+
import Animated, { useSharedValue, useDerivedValue, useAnimatedStyle, withTiming, Easing } from 'react-native-reanimated';
|
|
4
4
|
import { useSafeAreaInsets } from '@idealyst/theme';
|
|
5
5
|
import { DialogProps } from './types';
|
|
6
6
|
import { dialogStyles } from './Dialog.styles';
|
|
@@ -51,27 +51,35 @@ const Dialog = forwardRef<View, DialogProps>(({
|
|
|
51
51
|
// Get safe area insets
|
|
52
52
|
const insets = useSafeAreaInsets();
|
|
53
53
|
|
|
54
|
-
//
|
|
55
|
-
const
|
|
54
|
+
// Animated keyboard height for smooth keyboard avoidance
|
|
55
|
+
const keyboardHeight = useSharedValue(0);
|
|
56
56
|
const { height: screenHeight } = useWindowDimensions();
|
|
57
57
|
|
|
58
58
|
useEffect(() => {
|
|
59
|
-
if (!avoidKeyboard || !open)
|
|
59
|
+
if (!avoidKeyboard || !open) {
|
|
60
|
+
// Animate back to 0 when dialog closes or avoidKeyboard is disabled
|
|
61
|
+
keyboardHeight.value = withTiming(0, { duration: 250 });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
60
64
|
|
|
61
65
|
const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
|
|
62
66
|
const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';
|
|
63
67
|
|
|
64
68
|
const showSub = Keyboard.addListener(showEvent, (e) => {
|
|
65
|
-
|
|
69
|
+
const duration = Platform.OS === 'ios' ? e.duration : 250;
|
|
70
|
+
keyboardHeight.value = withTiming(e.endCoordinates.height, { duration });
|
|
66
71
|
});
|
|
67
72
|
|
|
68
|
-
const hideSub = Keyboard.addListener(hideEvent, () => {
|
|
69
|
-
|
|
73
|
+
const hideSub = Keyboard.addListener(hideEvent, (e) => {
|
|
74
|
+
const duration = Platform.OS === 'ios' ? (e?.duration ?? 250) : 250;
|
|
75
|
+
keyboardHeight.value = withTiming(0, { duration });
|
|
70
76
|
});
|
|
71
77
|
|
|
72
78
|
return () => {
|
|
73
79
|
showSub.remove();
|
|
74
80
|
hideSub.remove();
|
|
81
|
+
// Animate back to 0 on cleanup so we don't get stuck at a stale value
|
|
82
|
+
keyboardHeight.value = withTiming(0, { duration: 250 });
|
|
75
83
|
};
|
|
76
84
|
}, [avoidKeyboard, open]);
|
|
77
85
|
|
|
@@ -168,48 +176,69 @@ const Dialog = forwardRef<View, DialogProps>(({
|
|
|
168
176
|
bottom: 0,
|
|
169
177
|
};
|
|
170
178
|
|
|
171
|
-
//
|
|
172
|
-
// Top: always safe area + padding
|
|
173
|
-
// Bottom: safe area + padding (no keyboard) or keyboard + padding (with keyboard)
|
|
179
|
+
// Derived bottom offset — animates smoothly with keyboard
|
|
174
180
|
const topOffset = insets.top + paddingProp;
|
|
175
|
-
const bottomOffset =
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
+
const bottomOffset = useDerivedValue(() => {
|
|
182
|
+
'worklet';
|
|
183
|
+
return keyboardHeight.value > 0
|
|
184
|
+
? keyboardHeight.value + paddingProp
|
|
185
|
+
: insets.bottom + paddingProp;
|
|
186
|
+
});
|
|
181
187
|
|
|
182
|
-
//
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
188
|
+
// Animated style for the positioning wrapper
|
|
189
|
+
const positioningStyle = useAnimatedStyle(() => {
|
|
190
|
+
'worklet';
|
|
191
|
+
return {
|
|
192
|
+
bottom: bottomOffset.value,
|
|
193
|
+
};
|
|
194
|
+
});
|
|
186
195
|
|
|
187
196
|
// Resolve explicit height (number or percentage string)
|
|
188
197
|
const resolvedHeight = typeof height === 'string'
|
|
189
198
|
? height.endsWith('%')
|
|
190
|
-
? (parseFloat(height) / 100) *
|
|
199
|
+
? (parseFloat(height) / 100) * screenHeight // approximate; animated version below handles it
|
|
191
200
|
: parseFloat(height)
|
|
192
201
|
: height;
|
|
193
202
|
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
const
|
|
203
|
+
// Only apply flex: 1 to content when the dialog has a definite height to flex against.
|
|
204
|
+
// Without a definite height, flex: 1 collapses content instead of wrapping naturally.
|
|
205
|
+
const hasDefiniteHeight = Boolean(resolvedHeight || maxContentHeight);
|
|
206
|
+
|
|
207
|
+
// Static container styles (not dependent on keyboard)
|
|
208
|
+
const staticContainerStyle = {
|
|
197
209
|
...containerStyle,
|
|
198
|
-
maxHeight: effectiveMaxHeight,
|
|
199
|
-
height: resolvedHeight
|
|
200
|
-
? Math.min(resolvedHeight, effectiveMaxHeight)
|
|
201
|
-
: maxContentHeight
|
|
202
|
-
? effectiveMaxHeight
|
|
203
|
-
: undefined,
|
|
204
210
|
flex: undefined,
|
|
205
211
|
};
|
|
206
212
|
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
213
|
+
// Animated maxHeight/height that responds to keyboard changes
|
|
214
|
+
const dialogSizeStyle = useAnimatedStyle(() => {
|
|
215
|
+
'worklet';
|
|
216
|
+
const currentBottom = bottomOffset.value;
|
|
217
|
+
const maxAvailable = screenHeight - topOffset - currentBottom;
|
|
218
|
+
const effectiveMax = maxContentHeight
|
|
219
|
+
? Math.min(maxContentHeight, maxAvailable)
|
|
220
|
+
: maxAvailable;
|
|
221
|
+
|
|
222
|
+
const result: { maxHeight: number; height?: number } = {
|
|
223
|
+
maxHeight: effectiveMax,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
if (resolvedHeight) {
|
|
227
|
+
result.height = Math.min(resolvedHeight, effectiveMax);
|
|
228
|
+
} else if (maxContentHeight) {
|
|
229
|
+
result.height = effectiveMax;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return result;
|
|
233
|
+
});
|
|
210
234
|
|
|
211
235
|
const dialogContainer = (
|
|
212
|
-
<Animated.View
|
|
236
|
+
<Animated.View
|
|
237
|
+
ref={ref as any}
|
|
238
|
+
style={[staticContainerStyle, style, dialogSizeStyle, containerAnimatedStyle]}
|
|
239
|
+
nativeID={id}
|
|
240
|
+
{...nativeA11yProps}
|
|
241
|
+
>
|
|
213
242
|
{(title || showCloseButton) && (
|
|
214
243
|
<View style={[headerStyle, { flexShrink: 0 }]}>
|
|
215
244
|
{title && (
|
|
@@ -259,25 +288,24 @@ const Dialog = forwardRef<View, DialogProps>(({
|
|
|
259
288
|
</TouchableWithoutFeedback>
|
|
260
289
|
)}
|
|
261
290
|
{/* Dialog content - positioned absolute, accounts for keyboard and safe areas */}
|
|
262
|
-
<View
|
|
263
|
-
style={{
|
|
291
|
+
<Animated.View
|
|
292
|
+
style={[{
|
|
264
293
|
position: 'absolute',
|
|
265
294
|
top: topOffset,
|
|
266
295
|
left: 0,
|
|
267
296
|
right: 0,
|
|
268
|
-
bottom: bottomOffset,
|
|
269
297
|
alignItems: 'center',
|
|
270
298
|
justifyContent: 'center',
|
|
271
299
|
zIndex: 1001,
|
|
272
|
-
}}
|
|
300
|
+
}, positioningStyle]}
|
|
273
301
|
pointerEvents="box-none"
|
|
274
302
|
>
|
|
275
303
|
{dialogContainer}
|
|
276
|
-
</View>
|
|
304
|
+
</Animated.View>
|
|
277
305
|
</Modal>
|
|
278
306
|
);
|
|
279
307
|
});
|
|
280
308
|
|
|
281
309
|
Dialog.displayName = 'Dialog';
|
|
282
310
|
|
|
283
|
-
export default Dialog;
|
|
311
|
+
export default Dialog;
|
|
@@ -70,8 +70,6 @@ const IconButton = forwardRef<IdealystElement, IconButtonProps>((props, ref) =>
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
73
|
-
e.preventDefault();
|
|
74
|
-
e.stopPropagation();
|
|
75
73
|
if (!isDisabled && pressHandler) {
|
|
76
74
|
pressHandler(createPressEvent(e));
|
|
77
75
|
}
|
|
@@ -28,24 +28,18 @@ const Pressable = forwardRef<IdealystElement, PressableProps>(({
|
|
|
28
28
|
const [_isPressed, setIsPressed] = useState(false);
|
|
29
29
|
|
|
30
30
|
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
|
31
|
-
e.preventDefault();
|
|
32
|
-
e.stopPropagation();
|
|
33
31
|
if (disabled) return;
|
|
34
32
|
setIsPressed(true);
|
|
35
33
|
onPressIn?.(createPressEvent(e as React.MouseEvent<HTMLElement>, 'pressIn'));
|
|
36
34
|
}, [disabled, onPressIn]);
|
|
37
35
|
|
|
38
36
|
const handleMouseUp = useCallback((e: React.MouseEvent) => {
|
|
39
|
-
e.preventDefault();
|
|
40
|
-
e.stopPropagation();
|
|
41
37
|
if (disabled) return;
|
|
42
38
|
setIsPressed(false);
|
|
43
39
|
onPressOut?.(createPressEvent(e as React.MouseEvent<HTMLElement>, 'pressOut'));
|
|
44
40
|
}, [disabled, onPressOut]);
|
|
45
41
|
|
|
46
42
|
const handleClick = useCallback((e: React.MouseEvent) => {
|
|
47
|
-
e.preventDefault();
|
|
48
|
-
e.stopPropagation();
|
|
49
43
|
if (disabled) return;
|
|
50
44
|
onPress?.(createPressEvent(e as React.MouseEvent<HTMLElement>));
|
|
51
45
|
}, [disabled, onPress]);
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
1
|
+
import React, { useRef, useState } from 'react';
|
|
2
2
|
import { Screen, View, Text, Button, Menu, MenuItem } from '@idealyst/components';
|
|
3
3
|
|
|
4
4
|
export const MenuExamples: React.FC = () => {
|
|
5
5
|
const [basicMenuOpen, setBasicMenuOpen] = useState(false);
|
|
6
|
+
const [anchorMenuOpen, setAnchorMenuOpen] = useState(false);
|
|
6
7
|
const [placementMenuOpen, setPlacementMenuOpen] = useState(false);
|
|
7
8
|
const [iconNameMenuOpen, setIconNameMenuOpen] = useState(false);
|
|
8
9
|
const [intentMenuOpen, setIntentMenuOpen] = useState(false);
|
|
9
10
|
const [separatorMenuOpen, setSeparatorMenuOpen] = useState(false);
|
|
10
11
|
const [disabledMenuOpen, setDisabledMenuOpen] = useState(false);
|
|
12
|
+
const [rightEdgeMenuOpen, setRightEdgeMenuOpen] = useState(false);
|
|
13
|
+
const [bottomEdgeMenuOpen, setBottomEdgeMenuOpen] = useState(false);
|
|
14
|
+
|
|
15
|
+
const anchorRef = useRef(null);
|
|
11
16
|
|
|
12
17
|
const [selectedAction, setSelectedAction] = useState<string>('');
|
|
13
18
|
|
|
@@ -59,7 +64,7 @@ export const MenuExamples: React.FC = () => {
|
|
|
59
64
|
)}
|
|
60
65
|
|
|
61
66
|
<View gap="md">
|
|
62
|
-
<Text typography="h5">Basic Menu</Text>
|
|
67
|
+
<Text typography="h5">Basic Menu (Children Mode)</Text>
|
|
63
68
|
<Menu
|
|
64
69
|
items={basicItems}
|
|
65
70
|
open={basicMenuOpen}
|
|
@@ -71,6 +76,27 @@ export const MenuExamples: React.FC = () => {
|
|
|
71
76
|
</Menu>
|
|
72
77
|
</View>
|
|
73
78
|
|
|
79
|
+
<View gap="md">
|
|
80
|
+
<Text typography="h5">Anchor Mode</Text>
|
|
81
|
+
<Text typography="body2" color="secondary">
|
|
82
|
+
The menu appears next to the anchor element, not the button.
|
|
83
|
+
</Text>
|
|
84
|
+
<View direction="row" gap="md" align="center">
|
|
85
|
+
<Button type="outlined" onPress={() => setAnchorMenuOpen(true)}>
|
|
86
|
+
Open Anchored Menu
|
|
87
|
+
</Button>
|
|
88
|
+
<View ref={anchorRef} style={{ padding: 8, borderWidth: 1, borderStyle: 'dashed', borderColor: '#999', borderRadius: 8 }}>
|
|
89
|
+
<Text color="secondary">Anchor</Text>
|
|
90
|
+
</View>
|
|
91
|
+
</View>
|
|
92
|
+
<Menu
|
|
93
|
+
items={iconNameItems}
|
|
94
|
+
anchor={anchorRef}
|
|
95
|
+
open={anchorMenuOpen}
|
|
96
|
+
onOpenChange={setAnchorMenuOpen}
|
|
97
|
+
/>
|
|
98
|
+
</View>
|
|
99
|
+
|
|
74
100
|
<View gap="md">
|
|
75
101
|
<Text typography="h5">Placement Options</Text>
|
|
76
102
|
<Menu
|
|
@@ -136,6 +162,37 @@ export const MenuExamples: React.FC = () => {
|
|
|
136
162
|
</Button>
|
|
137
163
|
</Menu>
|
|
138
164
|
</View>
|
|
165
|
+
|
|
166
|
+
<Text typography="h4">Edge-of-Screen Tests</Text>
|
|
167
|
+
<Text typography="body2" color="secondary">
|
|
168
|
+
These menus should flip or clamp to stay within the viewport.
|
|
169
|
+
</Text>
|
|
170
|
+
|
|
171
|
+
<View style={{ alignItems: 'flex-end' }}>
|
|
172
|
+
<Menu
|
|
173
|
+
items={iconNameItems}
|
|
174
|
+
open={rightEdgeMenuOpen}
|
|
175
|
+
onOpenChange={setRightEdgeMenuOpen}
|
|
176
|
+
placement="bottom-end"
|
|
177
|
+
>
|
|
178
|
+
<Button type="outlined">
|
|
179
|
+
Right Edge
|
|
180
|
+
</Button>
|
|
181
|
+
</Menu>
|
|
182
|
+
</View>
|
|
183
|
+
|
|
184
|
+
<View style={{ marginTop: 200 }}>
|
|
185
|
+
<Menu
|
|
186
|
+
items={separatorItems}
|
|
187
|
+
open={bottomEdgeMenuOpen}
|
|
188
|
+
onOpenChange={setBottomEdgeMenuOpen}
|
|
189
|
+
placement="bottom-start"
|
|
190
|
+
>
|
|
191
|
+
<Button type="outlined">
|
|
192
|
+
Bottom Edge (scroll down)
|
|
193
|
+
</Button>
|
|
194
|
+
</Menu>
|
|
195
|
+
</View>
|
|
139
196
|
</View>
|
|
140
197
|
</Screen>
|
|
141
198
|
);
|
|
@@ -120,28 +120,42 @@ const calculatePosition = (
|
|
|
120
120
|
position.width = anchorRect.width;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
// Clamp to viewport bounds (viewport-relative for fixed positioning)
|
|
124
123
|
const padding = 8;
|
|
125
|
-
position.left = Math.max(padding, Math.min(position.left, vpWidth - contentSize.width - padding));
|
|
126
|
-
position.top = Math.max(padding, Math.min(position.top, vpHeight - contentSize.height - padding));
|
|
127
124
|
|
|
128
|
-
// Flip
|
|
125
|
+
// Flip placement if it overflows (must run BEFORE clamping)
|
|
129
126
|
const isAbove = placement.startsWith('top');
|
|
130
127
|
const isBelow = placement.startsWith('bottom');
|
|
128
|
+
const isLeft = placement.startsWith('left');
|
|
129
|
+
const isRight = placement.startsWith('right');
|
|
130
|
+
|
|
131
131
|
if (isBelow && position.top + contentSize.height > vpHeight - padding) {
|
|
132
|
-
// Not enough space below — try above
|
|
133
132
|
const aboveTop = anchorRect.top - contentSize.height - offset;
|
|
134
133
|
if (aboveTop >= padding) {
|
|
135
134
|
position.top = aboveTop;
|
|
136
135
|
}
|
|
137
136
|
} else if (isAbove && position.top < padding) {
|
|
138
|
-
// Not enough space above — try below
|
|
139
137
|
const belowTop = anchorRect.bottom + offset;
|
|
140
138
|
if (belowTop + contentSize.height <= vpHeight - padding) {
|
|
141
139
|
position.top = belowTop;
|
|
142
140
|
}
|
|
143
141
|
}
|
|
144
142
|
|
|
143
|
+
if (isRight && position.left + contentSize.width > vpWidth - padding) {
|
|
144
|
+
const leftPos = anchorRect.left - contentSize.width - offset;
|
|
145
|
+
if (leftPos >= padding) {
|
|
146
|
+
position.left = leftPos;
|
|
147
|
+
}
|
|
148
|
+
} else if (isLeft && position.left < padding) {
|
|
149
|
+
const rightPos = anchorRect.right + offset;
|
|
150
|
+
if (rightPos + contentSize.width <= vpWidth - padding) {
|
|
151
|
+
position.left = rightPos;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Clamp to viewport bounds as final safety net
|
|
156
|
+
position.left = Math.max(padding, Math.min(position.left, vpWidth - contentSize.width - padding));
|
|
157
|
+
position.top = Math.max(padding, Math.min(position.top, vpHeight - contentSize.height - padding));
|
|
158
|
+
|
|
145
159
|
return position;
|
|
146
160
|
};
|
|
147
161
|
|
|
@@ -160,19 +174,20 @@ export const PositionedPortal: React.FC<PositionedPortalProps> = ({
|
|
|
160
174
|
const [position, setPosition] = useState<Position>({ top: 0, left: 0 });
|
|
161
175
|
const [isPositioned, setIsPositioned] = useState(false);
|
|
162
176
|
|
|
163
|
-
// Calculate position
|
|
177
|
+
// Calculate position
|
|
164
178
|
const updatePosition = useCallback(() => {
|
|
165
179
|
if (!contentRef.current || !anchor.current) return;
|
|
166
180
|
|
|
167
181
|
const anchorRect = anchor.current.getBoundingClientRect();
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
//
|
|
171
|
-
|
|
182
|
+
// Use scrollWidth/scrollHeight for intrinsic content size —
|
|
183
|
+
// getBoundingClientRect() can be wrong when the element is at position 0,0
|
|
184
|
+
// because the container div has no explicit width and may stretch.
|
|
185
|
+
const contentWidth = contentRef.current.scrollWidth;
|
|
186
|
+
const contentHeight = contentRef.current.scrollHeight;
|
|
172
187
|
|
|
173
188
|
const newPosition = calculatePosition(
|
|
174
189
|
anchorRect,
|
|
175
|
-
{ width:
|
|
190
|
+
{ width: contentWidth, height: contentHeight },
|
|
176
191
|
placement,
|
|
177
192
|
offset,
|
|
178
193
|
matchWidth
|
|
@@ -182,26 +197,11 @@ export const PositionedPortal: React.FC<PositionedPortalProps> = ({
|
|
|
182
197
|
setIsPositioned(true);
|
|
183
198
|
}, [anchor, placement, offset, matchWidth]);
|
|
184
199
|
|
|
185
|
-
//
|
|
186
|
-
useEffect(() => {
|
|
187
|
-
if (!open || !contentRef.current) return;
|
|
188
|
-
|
|
189
|
-
const observer = new ResizeObserver(() => {
|
|
190
|
-
updatePosition();
|
|
191
|
-
});
|
|
192
|
-
observer.observe(contentRef.current);
|
|
193
|
-
|
|
194
|
-
return () => observer.disconnect();
|
|
195
|
-
}, [open, updatePosition]);
|
|
196
|
-
|
|
197
|
-
// Initial positioning after portal mounts
|
|
200
|
+
// Position after DOM is ready
|
|
198
201
|
useLayoutEffect(() => {
|
|
199
202
|
if (open) {
|
|
200
|
-
// Double-rAF to ensure portal content is in the DOM and laid out
|
|
201
203
|
const rafId = requestAnimationFrame(() => {
|
|
202
|
-
|
|
203
|
-
updatePosition();
|
|
204
|
-
});
|
|
204
|
+
updatePosition();
|
|
205
205
|
});
|
|
206
206
|
return () => cancelAnimationFrame(rafId);
|
|
207
207
|
} else {
|
|
@@ -209,10 +209,12 @@ export const PositionedPortal: React.FC<PositionedPortalProps> = ({
|
|
|
209
209
|
}
|
|
210
210
|
}, [open, updatePosition]);
|
|
211
211
|
|
|
212
|
-
//
|
|
212
|
+
// Update position on scroll/resize
|
|
213
213
|
useEffect(() => {
|
|
214
214
|
if (!open) return;
|
|
215
215
|
|
|
216
|
+
updatePosition();
|
|
217
|
+
|
|
216
218
|
const handleUpdate = () => updatePosition();
|
|
217
219
|
window.addEventListener('resize', handleUpdate);
|
|
218
220
|
window.addEventListener('scroll', handleUpdate, true);
|
|
@@ -33,21 +33,25 @@ const noop = () => {};
|
|
|
33
33
|
/**
|
|
34
34
|
* Wraps a React Native GestureResponderEvent into a standardized PressEvent.
|
|
35
35
|
* Pass the component's ref object as `targetRef` so consumers can use it for anchoring.
|
|
36
|
+
* preventDefault() and stopPropagation() are no-ops on native but still update flags.
|
|
36
37
|
*/
|
|
37
38
|
export function createPressEvent(
|
|
38
39
|
event: GestureResponderEvent,
|
|
39
40
|
type: PressEvent['type'] = 'press',
|
|
40
41
|
targetRef?: RefObject<IdealystElement>
|
|
41
42
|
): PressEvent {
|
|
43
|
+
let defaultPrevented = false;
|
|
44
|
+
let propagationStopped = false;
|
|
45
|
+
|
|
42
46
|
return {
|
|
43
47
|
nativeEvent: event.nativeEvent,
|
|
44
48
|
timestamp: event.nativeEvent.timestamp,
|
|
45
|
-
defaultPrevented
|
|
46
|
-
propagationStopped
|
|
49
|
+
get defaultPrevented() { return defaultPrevented; },
|
|
50
|
+
get propagationStopped() { return propagationStopped; },
|
|
47
51
|
type,
|
|
48
52
|
targetRef: targetRef ?? { current: event.target },
|
|
49
|
-
preventDefault
|
|
50
|
-
stopPropagation
|
|
53
|
+
preventDefault() { defaultPrevented = true; },
|
|
54
|
+
stopPropagation() { propagationStopped = true; },
|
|
51
55
|
};
|
|
52
56
|
}
|
|
53
57
|
|
|
@@ -23,21 +23,32 @@ type ReactFormEvent = React.FormEvent<HTMLFormElement>;
|
|
|
23
23
|
type ReactUIEvent = React.UIEvent<HTMLElement>;
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
* Wraps a React mouse/click event into a standardized PressEvent
|
|
26
|
+
* Wraps a React mouse/click event into a standardized PressEvent.
|
|
27
|
+
* preventDefault() and stopPropagation() call through to the underlying
|
|
28
|
+
* React event and also update the PressEvent's own flags.
|
|
27
29
|
*/
|
|
28
30
|
export function createPressEvent(
|
|
29
31
|
event: ReactMouseEvent,
|
|
30
32
|
type: PressEvent['type'] = 'press'
|
|
31
33
|
): PressEvent {
|
|
34
|
+
let defaultPrevented = event.defaultPrevented;
|
|
35
|
+
let propagationStopped = false;
|
|
36
|
+
|
|
32
37
|
return {
|
|
33
38
|
nativeEvent: event.nativeEvent,
|
|
34
39
|
timestamp: event.timeStamp,
|
|
35
|
-
defaultPrevented
|
|
36
|
-
propagationStopped
|
|
40
|
+
get defaultPrevented() { return defaultPrevented; },
|
|
41
|
+
get propagationStopped() { return propagationStopped; },
|
|
37
42
|
type,
|
|
38
43
|
targetRef: { current: event.currentTarget },
|
|
39
|
-
preventDefault
|
|
40
|
-
|
|
44
|
+
preventDefault() {
|
|
45
|
+
defaultPrevented = true;
|
|
46
|
+
event.preventDefault();
|
|
47
|
+
},
|
|
48
|
+
stopPropagation() {
|
|
49
|
+
propagationStopped = true;
|
|
50
|
+
event.stopPropagation();
|
|
51
|
+
},
|
|
41
52
|
};
|
|
42
53
|
}
|
|
43
54
|
|