|
67 | 67 | // Private |
68 | 68 | let _rootEl: HTMLElement; |
69 | 69 | let _popoverEl: HTMLElement; |
| 70 | + const _needsManualPositioning = |
| 71 | + typeof document !== "undefined" && |
| 72 | + !("anchorName" in document.documentElement.style); |
| 73 | + let _positionRafId: number | null = null; |
70 | 74 |
|
71 | 75 | // Reactive |
72 | 76 | let _targetEl: HTMLElement; |
|
122 | 126 | }); |
123 | 127 |
|
124 | 128 | onDestroy(() => { |
| 129 | + stopManualPositioning(); |
125 | 130 | window.removeEventListener("resize", updateAutoPosition); |
126 | 131 | // true was passed when the listener was added, so it's necesary to be passed here as well |
127 | 132 | window.removeEventListener("popstate", handleUrlChange, true); |
128 | 133 | }); |
129 | 134 |
|
130 | 135 | // Functions |
131 | 136 |
|
| 137 | + function updatePopoverPosition() { |
| 138 | + if (!_isOpen || !_targetEl || !_popoverEl) return; |
| 139 | +
|
| 140 | + const targetRect = _targetEl.getBoundingClientRect(); |
| 141 | + const xOffset = hoffset ? parseFloat(hoffset) : 0; |
| 142 | + const yOffset = voffset ? parseFloat(voffset) : 3; |
| 143 | +
|
| 144 | + // Recalculate auto position based on current viewport space |
| 145 | + if (position === "auto") { |
| 146 | + const popoverRect = _popoverEl.getBoundingClientRect(); |
| 147 | + const spaceAbove = targetRect.top; |
| 148 | + const spaceBelow = window.innerHeight - targetRect.bottom; |
| 149 | +
|
| 150 | + _autoPosition = |
| 151 | + spaceBelow < popoverRect.height && spaceAbove > spaceBelow |
| 152 | + ? "above" |
| 153 | + : "below"; |
| 154 | + } |
| 155 | +
|
| 156 | + const isAbove = |
| 157 | + position === "above" || |
| 158 | + (position === "auto" && _autoPosition === "above"); |
| 159 | +
|
| 160 | + if (isAbove) { |
| 161 | + _popoverEl.style.top = `${targetRect.top - yOffset}px`; |
| 162 | + _popoverEl.style.left = `${targetRect.left + xOffset}px`; |
| 163 | + _popoverEl.style.transform = "translateY(-100%)"; |
| 164 | + } else { |
| 165 | + _popoverEl.style.top = `${targetRect.bottom + yOffset}px`; |
| 166 | + _popoverEl.style.left = `${targetRect.left + xOffset}px`; |
| 167 | + _popoverEl.style.transform = ""; |
| 168 | + } |
| 169 | + } |
| 170 | +
|
| 171 | + function startManualPositioning() { |
| 172 | + if (!_needsManualPositioning) return; |
| 173 | +
|
| 174 | + const loop = () => { |
| 175 | + updatePopoverPosition(); |
| 176 | + _positionRafId = requestAnimationFrame(loop); |
| 177 | + }; |
| 178 | + _positionRafId = requestAnimationFrame(loop); |
| 179 | + } |
| 180 | +
|
| 181 | + function stopManualPositioning() { |
| 182 | + if (_positionRafId !== null) { |
| 183 | + cancelAnimationFrame(_positionRafId); |
| 184 | + _positionRafId = null; |
| 185 | + } |
| 186 | + } |
| 187 | +
|
132 | 188 | function isPopoverOpen(): boolean { |
133 | 189 | try { |
134 | 190 | return _popoverEl.matches(":popover-open"); |
|
188 | 244 | } |
189 | 245 | } |
190 | 246 |
|
191 | | - function handleNativeToggle(e: Event) { |
192 | | - const toggleEvent = e as Event & { newState?: "open" | "closed" }; |
| 247 | + function handleNativeToggle(toggleEvent: ToggleEvent) { |
193 | 248 | if (toggleEvent.newState === "open") { |
194 | 249 | _isOpen = true; |
195 | 250 | } else if (toggleEvent.newState === "closed") { |
|
203 | 258 | // Dispatch _open/_close events for consumer components |
204 | 259 | // (MenuButton, AppHeader, AppHeaderMenu, Dropdown) |
205 | 260 | if (_isOpen) { |
206 | | - dispatch(_rootEl, "_open", {}, { bubbles: true }); |
| 261 | + dispatch(_rootEl, "_open"); |
207 | 262 | requestAnimationFrame(updateAutoPosition); // same vs await tick(), make sure popover element is fully rendered before we measure its dimension |
| 263 | + startManualPositioning(); |
208 | 264 | } else { |
209 | | - _targetEl.focus(); |
210 | | - dispatch(_rootEl, "_close", {}, { bubbles: true }); |
| 265 | + stopManualPositioning(); |
| 266 | + _targetEl?.focus(); |
| 267 | + dispatch(_rootEl, "_close"); |
211 | 268 | } |
212 | 269 | } |
213 | 270 |
|
214 | 271 | function closePopover() { |
215 | 272 | if (_isOpen) { |
216 | 273 | _popoverEl?.hidePopover(); // browser will fire and trigger handleNativeToggle |
| 274 | + // If the browser doesn't support the API we have to trigger the toggle event manually. |
| 275 | + if (_needsManualPositioning) { |
| 276 | + const event = new ToggleEvent("toggle", { |
| 277 | + bubbles: true, |
| 278 | + newState: "closed", |
| 279 | + oldState: "open", |
| 280 | + }); |
| 281 | + handleNativeToggle(event); // in case the browser doesn't fire toggle event, we need to manually update the state |
| 282 | + } |
217 | 283 | } |
218 | 284 | } |
219 | 285 |
|
|
227 | 293 | _popoverEl.hidePopover(); |
228 | 294 | _isOpen = false; |
229 | 295 | } else { |
| 296 | + // If the Popover API is not supported, we need to manually close other |
| 297 | + // popovers before opening a new one. |
| 298 | + if (_needsManualPositioning) { |
| 299 | + document.body.dispatchEvent( |
| 300 | + new CustomEvent("goa:closePopover", { |
| 301 | + detail: { target: _targetEl }, |
| 302 | + bubbles: true, |
| 303 | + }), |
| 304 | + ); |
| 305 | + } |
230 | 306 | _popoverEl.showPopover(); |
231 | 307 | _isOpen = true; |
232 | 308 | requestAnimationFrame(updateAutoPosition); |
233 | 309 | } |
234 | 310 | } |
235 | 311 |
|
236 | 312 | function updateAutoPosition() { |
237 | | - if (!_isOpen || !_targetEl || !_popoverEl) { |
238 | | - return; |
239 | | - } |
240 | | -
|
241 | | - if (position !== "auto") { |
| 313 | + if (position !== "auto" || !_isOpen || !_targetEl || !_popoverEl) { |
242 | 314 | return; |
243 | 315 | } |
244 | 316 |
|
|
301 | 373 | style={styles( |
302 | 374 | style("width", position !== "right" ? width : undefined), |
303 | 375 | style("min-width", minwidth), |
304 | | - style("max-width", position !== "right" && width ? `max(${width}, ${maxwidth})` : maxwidth), |
| 376 | + style( |
| 377 | + "max-width", |
| 378 | + position !== "right" && width ? `max(${width}, ${maxwidth})` : maxwidth, |
| 379 | + ), |
305 | 380 | style("padding", _padded ? "var(--goa-space-m)" : "0"), |
306 | 381 | )} |
307 | 382 | > |
|
355 | 430 | filter: var(--goa-popover-shadow, none); |
356 | 431 | border: var(--goa-popover-border, none); |
357 | 432 | margin: 0; |
358 | | -
|
359 | 433 | position-anchor: --goa-popover-target; |
360 | 434 | inset-block-start: anchor(bottom); |
361 | 435 | inset-inline-start: anchor(left); |
|
0 commit comments