@idealyst/components 1.2.134 → 1.2.136

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.134",
3
+ "version": "1.2.136",
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.134",
59
+ "@idealyst/theme": "^1.2.136",
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.134",
115
- "@idealyst/tooling": "^1.2.134",
114
+ "@idealyst/theme": "^1.2.136",
115
+ "@idealyst/tooling": "^1.2.136",
116
116
  "@mdi/react": "^1.6.1",
117
117
  "@types/react": "^19.1.0",
118
118
  "react": "^19.1.0",
@@ -32,87 +32,85 @@ const calculatePosition = (
32
32
  offset: number,
33
33
  matchWidth: boolean
34
34
  ): Position => {
35
- const viewport = {
36
- width: window.innerWidth,
37
- height: window.innerHeight,
38
- scrollX: window.scrollX,
39
- scrollY: window.scrollY,
40
- };
35
+ // anchorRect from getBoundingClientRect() is viewport-relative,
36
+ // which is exactly what we need for position: fixed.
37
+ const vpWidth = window.innerWidth;
38
+ const vpHeight = window.innerHeight;
41
39
 
42
40
  let position: Position = { top: 0, left: 0 };
43
41
 
44
- // Calculate initial position based on placement
42
+ // Calculate initial position based on placement (viewport-relative for fixed positioning)
45
43
  switch (placement) {
46
44
  case 'top':
47
45
  position = {
48
- top: anchorRect.top + viewport.scrollY - contentSize.height - offset,
49
- left: anchorRect.left + viewport.scrollX + anchorRect.width / 2 - contentSize.width / 2,
46
+ top: anchorRect.top - contentSize.height - offset,
47
+ left: anchorRect.left + anchorRect.width / 2 - contentSize.width / 2,
50
48
  };
51
49
  break;
52
50
  case 'top-start':
53
51
  position = {
54
- top: anchorRect.top + viewport.scrollY - contentSize.height - offset,
55
- left: anchorRect.left + viewport.scrollX,
52
+ top: anchorRect.top - contentSize.height - offset,
53
+ left: anchorRect.left,
56
54
  };
57
55
  break;
58
56
  case 'top-end':
59
57
  position = {
60
- top: anchorRect.top + viewport.scrollY - contentSize.height - offset,
61
- left: anchorRect.right + viewport.scrollX - contentSize.width,
58
+ top: anchorRect.top - contentSize.height - offset,
59
+ left: anchorRect.right - contentSize.width,
62
60
  };
63
61
  break;
64
62
  case 'bottom':
65
63
  position = {
66
- top: anchorRect.bottom + viewport.scrollY + offset,
67
- left: anchorRect.left + viewport.scrollX + anchorRect.width / 2 - contentSize.width / 2,
64
+ top: anchorRect.bottom + offset,
65
+ left: anchorRect.left + anchorRect.width / 2 - contentSize.width / 2,
68
66
  };
69
67
  break;
70
68
  case 'bottom-start':
71
69
  position = {
72
- top: anchorRect.bottom + viewport.scrollY + offset,
73
- left: anchorRect.left + viewport.scrollX,
70
+ top: anchorRect.bottom + offset,
71
+ left: anchorRect.left,
74
72
  };
75
73
  break;
76
74
  case 'bottom-end':
77
75
  position = {
78
- top: anchorRect.bottom + viewport.scrollY + offset,
79
- left: anchorRect.right + viewport.scrollX - contentSize.width,
76
+ top: anchorRect.bottom + offset,
77
+ left: anchorRect.right - contentSize.width,
80
78
  };
81
79
  break;
82
80
  case 'left':
83
81
  position = {
84
- top: anchorRect.top + viewport.scrollY + anchorRect.height / 2 - contentSize.height / 2,
85
- left: anchorRect.left + viewport.scrollX - contentSize.width - offset,
82
+ top: anchorRect.top + anchorRect.height / 2 - contentSize.height / 2,
83
+ left: anchorRect.left - contentSize.width - offset,
86
84
  };
87
85
  break;
88
86
  case 'left-start':
89
87
  position = {
90
- top: anchorRect.top + viewport.scrollY,
91
- left: anchorRect.left + viewport.scrollX - contentSize.width - offset,
88
+ top: anchorRect.top,
89
+ left: anchorRect.left - contentSize.width - offset,
92
90
  };
93
91
  break;
94
92
  case 'left-end':
95
93
  position = {
96
- top: anchorRect.bottom + viewport.scrollY - contentSize.height,
97
- left: anchorRect.left + viewport.scrollX - contentSize.width - offset,
94
+ top: anchorRect.bottom - contentSize.height,
95
+ left: anchorRect.left - contentSize.width - offset,
98
96
  };
99
97
  break;
100
98
  case 'right':
101
99
  position = {
102
- top: anchorRect.top + viewport.scrollY + anchorRect.height / 2 - contentSize.height / 2,
103
- left: anchorRect.right + viewport.scrollX + offset,
100
+ top: anchorRect.top + anchorRect.height / 2 - contentSize.height / 2,
101
+ left: anchorRect.right + offset,
104
102
  };
105
103
  break;
106
104
  case 'right-start':
107
105
  position = {
108
- top: anchorRect.top + viewport.scrollY,
109
- left: anchorRect.right + viewport.scrollX + offset,
106
+ top: anchorRect.top,
107
+ left: anchorRect.right + offset,
110
108
  };
111
109
  break;
112
110
  case 'right-end':
113
111
  position = {
114
- top: anchorRect.bottom + viewport.scrollY - contentSize.height,
115
- left: anchorRect.right + viewport.scrollX + offset,
112
+ top: anchorRect.bottom - contentSize.height,
113
+ left: anchorRect.right + offset,
116
114
  };
117
115
  break;
118
116
  }
@@ -122,10 +120,27 @@ const calculatePosition = (
122
120
  position.width = anchorRect.width;
123
121
  }
124
122
 
125
- // Constrain to viewport
123
+ // Clamp to viewport bounds (viewport-relative for fixed positioning)
126
124
  const padding = 8;
127
- position.left = Math.max(padding, Math.min(position.left, viewport.width - contentSize.width - padding));
128
- position.top = Math.max(padding, Math.min(position.top, viewport.height + viewport.scrollY - contentSize.height - padding));
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
+
128
+ // Flip vertical placement if it overflows
129
+ const isAbove = placement.startsWith('top');
130
+ const isBelow = placement.startsWith('bottom');
131
+ if (isBelow && position.top + contentSize.height > vpHeight - padding) {
132
+ // Not enough space below — try above
133
+ const aboveTop = anchorRect.top - contentSize.height - offset;
134
+ if (aboveTop >= padding) {
135
+ position.top = aboveTop;
136
+ }
137
+ } else if (isAbove && position.top < padding) {
138
+ // Not enough space above — try below
139
+ const belowTop = anchorRect.bottom + offset;
140
+ if (belowTop + contentSize.height <= vpHeight - padding) {
141
+ position.top = belowTop;
142
+ }
143
+ }
129
144
 
130
145
  return position;
131
146
  };
@@ -145,16 +160,16 @@ export const PositionedPortal: React.FC<PositionedPortalProps> = ({
145
160
  const [position, setPosition] = useState<Position>({ top: 0, left: 0 });
146
161
  const [isPositioned, setIsPositioned] = useState(false);
147
162
 
148
- // Calculate position
163
+ // Calculate position from current DOM measurements
149
164
  const updatePosition = useCallback(() => {
150
- if (!contentRef.current || !anchor.current) {
151
- return;
152
- }
165
+ if (!contentRef.current || !anchor.current) return;
153
166
 
154
167
  const anchorRect = anchor.current.getBoundingClientRect();
155
168
  const contentRect = contentRef.current.getBoundingClientRect();
156
169
 
157
- // Use actual measured size from the DOM
170
+ // Skip if content hasn't laid out yet
171
+ if (contentRect.width === 0 && contentRect.height === 0) return;
172
+
158
173
  const newPosition = calculatePosition(
159
174
  anchorRect,
160
175
  { width: contentRect.width, height: contentRect.height },
@@ -167,14 +182,26 @@ export const PositionedPortal: React.FC<PositionedPortalProps> = ({
167
182
  setIsPositioned(true);
168
183
  }, [anchor, placement, offset, matchWidth]);
169
184
 
170
- // Position after DOM is ready
185
+ // Observe content size changes so we re-position once children lay out
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
171
198
  useLayoutEffect(() => {
172
199
  if (open) {
173
- // Use requestAnimationFrame to ensure ref is attached and layout is complete
200
+ // Double-rAF to ensure portal content is in the DOM and laid out
174
201
  const rafId = requestAnimationFrame(() => {
175
- if (contentRef.current && anchor.current) {
202
+ requestAnimationFrame(() => {
176
203
  updatePosition();
177
- }
204
+ });
178
205
  });
179
206
  return () => cancelAnimationFrame(rafId);
180
207
  } else {
@@ -182,12 +209,10 @@ export const PositionedPortal: React.FC<PositionedPortalProps> = ({
182
209
  }
183
210
  }, [open, updatePosition]);
184
211
 
185
- // Update position on scroll/resize
212
+ // Re-position on scroll/resize
186
213
  useEffect(() => {
187
214
  if (!open) return;
188
215
 
189
- updatePosition();
190
-
191
216
  const handleUpdate = () => updatePosition();
192
217
  window.addEventListener('resize', handleUpdate);
193
218
  window.addEventListener('scroll', handleUpdate, true);