@idealyst/components 1.2.135 → 1.2.137
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idealyst/components",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.137",
|
|
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.137",
|
|
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.137",
|
|
115
|
+
"@idealyst/tooling": "^1.2.137",
|
|
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
|
}
|
|
@@ -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
|
|
|
@@ -162,17 +176,18 @@ export const PositionedPortal: React.FC<PositionedPortalProps> = ({
|
|
|
162
176
|
|
|
163
177
|
// Calculate position
|
|
164
178
|
const updatePosition = useCallback(() => {
|
|
165
|
-
if (!contentRef.current || !anchor.current)
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
179
|
+
if (!contentRef.current || !anchor.current) return;
|
|
168
180
|
|
|
169
181
|
const anchorRect = anchor.current.getBoundingClientRect();
|
|
170
|
-
|
|
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;
|
|
171
187
|
|
|
172
|
-
// Use actual measured size from the DOM
|
|
173
188
|
const newPosition = calculatePosition(
|
|
174
189
|
anchorRect,
|
|
175
|
-
{ width:
|
|
190
|
+
{ width: contentWidth, height: contentHeight },
|
|
176
191
|
placement,
|
|
177
192
|
offset,
|
|
178
193
|
matchWidth
|
|
@@ -185,11 +200,8 @@ export const PositionedPortal: React.FC<PositionedPortalProps> = ({
|
|
|
185
200
|
// Position after DOM is ready
|
|
186
201
|
useLayoutEffect(() => {
|
|
187
202
|
if (open) {
|
|
188
|
-
// Use requestAnimationFrame to ensure ref is attached and layout is complete
|
|
189
203
|
const rafId = requestAnimationFrame(() => {
|
|
190
|
-
|
|
191
|
-
updatePosition();
|
|
192
|
-
}
|
|
204
|
+
updatePosition();
|
|
193
205
|
});
|
|
194
206
|
return () => cancelAnimationFrame(rafId);
|
|
195
207
|
} else {
|