-
Notifications
You must be signed in to change notification settings - Fork 36
eric fix(#3540): add manual positioning to Popover #3655
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -67,6 +67,10 @@ | |
| // Private | ||
| let _rootEl: HTMLElement; | ||
| let _popoverEl: HTMLElement; | ||
| const _needsManualPositioning = | ||
| typeof document !== "undefined" && | ||
| !("anchorName" in document.documentElement.style); | ||
| let _positionRafId: number | null = null; | ||
|
|
||
| // Reactive | ||
| let _targetEl: HTMLElement; | ||
|
|
@@ -122,13 +126,67 @@ | |
| }); | ||
|
|
||
| onDestroy(() => { | ||
| if (_needsManualPositioning || _positionRafId) { | ||
| stopManualPositioning(); | ||
| } | ||
| window.removeEventListener("resize", updateAutoPosition); | ||
| // true was passed when the listener was added, so it's necesary to be passed here as well | ||
| window.removeEventListener("popstate", handleUrlChange, true); | ||
| }); | ||
|
|
||
| // Functions | ||
|
|
||
| function updatePopoverPosition() { | ||
| if (!_isOpen || !_targetEl || !_popoverEl) return; | ||
|
|
||
| const targetRect = _targetEl.getBoundingClientRect(); | ||
| const xOffset = hoffset ? parseFloat(hoffset) : 0; | ||
| const yOffset = voffset ? parseFloat(voffset) : 3; | ||
|
willcodeforcoffee marked this conversation as resolved.
|
||
|
|
||
| // Recalculate auto position based on current viewport space | ||
| if (position === "auto") { | ||
| const popoverRect = _popoverEl.getBoundingClientRect(); | ||
| const spaceAbove = targetRect.top; | ||
| const spaceBelow = window.innerHeight - targetRect.bottom; | ||
|
|
||
| _autoPosition = | ||
| spaceBelow < popoverRect.height && spaceAbove > spaceBelow | ||
| ? "above" | ||
| : "below"; | ||
| } | ||
|
|
||
| const isAbove = | ||
| position === "above" || | ||
| (position === "auto" && _autoPosition === "above"); | ||
|
|
||
| if (isAbove) { | ||
| _popoverEl.style.top = `${targetRect.top - yOffset}px`; | ||
| _popoverEl.style.left = `${targetRect.left + xOffset}px`; | ||
| _popoverEl.style.transform = "translateY(-100%)"; | ||
| } else { | ||
| _popoverEl.style.top = `${targetRect.bottom + yOffset}px`; | ||
| _popoverEl.style.left = `${targetRect.left + xOffset}px`; | ||
| _popoverEl.style.transform = ""; | ||
| } | ||
|
Comment on lines
+158
to
+170
|
||
| } | ||
|
|
||
| function startManualPositioning() { | ||
| if (!_needsManualPositioning) return; | ||
|
|
||
| const loop = () => { | ||
| updatePopoverPosition(); | ||
| _positionRafId = requestAnimationFrame(loop); | ||
| }; | ||
| _positionRafId = requestAnimationFrame(loop); | ||
| } | ||
|
Comment on lines
+173
to
+181
|
||
|
|
||
| function stopManualPositioning() { | ||
| if (_positionRafId !== null) { | ||
| cancelAnimationFrame(_positionRafId); | ||
| _positionRafId = null; | ||
| } | ||
| } | ||
|
|
||
| function isPopoverOpen(): boolean { | ||
| try { | ||
| return _popoverEl.matches(":popover-open"); | ||
|
|
@@ -188,8 +246,7 @@ | |
| } | ||
| } | ||
|
|
||
| function handleNativeToggle(e: Event) { | ||
| const toggleEvent = e as Event & { newState?: "open" | "closed" }; | ||
| function handleNativeToggle(toggleEvent: ToggleEvent) { | ||
| if (toggleEvent.newState === "open") { | ||
| _isOpen = true; | ||
| } else if (toggleEvent.newState === "closed") { | ||
|
|
@@ -205,15 +262,30 @@ | |
| if (_isOpen) { | ||
| dispatch(_rootEl, "_open", {}, { bubbles: true }); | ||
| requestAnimationFrame(updateAutoPosition); // same vs await tick(), make sure popover element is fully rendered before we measure its dimension | ||
| if (_needsManualPositioning) { | ||
| startManualPositioning(); | ||
| } | ||
| } else { | ||
| _targetEl.focus(); | ||
| if (_needsManualPositioning || _positionRafId) { | ||
| stopManualPositioning(); | ||
| } | ||
| _targetEl?.focus(); | ||
| dispatch(_rootEl, "_close", {}, { bubbles: true }); | ||
| } | ||
| } | ||
|
|
||
| function closePopover() { | ||
| if (_isOpen) { | ||
| _popoverEl?.hidePopover(); // browser will fire and trigger handleNativeToggle | ||
| // If the browser doesn't support the API we have to trigger the toggle event manually. | ||
| if (_needsManualPositioning) { | ||
| const event = new ToggleEvent("toggle", { | ||
| bubbles: true, | ||
| newState: "closed", | ||
| oldState: "open", | ||
| }); | ||
| handleNativeToggle(event); // in case the browser doesn't fire toggle event, we need to manually update the state | ||
|
willcodeforcoffee marked this conversation as resolved.
|
||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -227,18 +299,19 @@ | |
| _popoverEl.hidePopover(); | ||
| _isOpen = false; | ||
| } else { | ||
| // If the Popover API is not supported, we need to manually close other | ||
| // popovers before opening a new one. | ||
| if (_needsManualPositioning) { | ||
| dispatch(document.body, "goa:closePopover", { target: _targetEl }); | ||
| } | ||
|
chrisolsen marked this conversation as resolved.
|
||
| _popoverEl.showPopover(); | ||
| _isOpen = true; | ||
| requestAnimationFrame(updateAutoPosition); | ||
| } | ||
| } | ||
|
|
||
| function updateAutoPosition() { | ||
| if (!_isOpen || !_targetEl || !_popoverEl) { | ||
| return; | ||
| } | ||
|
|
||
| if (position !== "auto") { | ||
| if (position !== "auto" || !_isOpen || !_targetEl || !_popoverEl) { | ||
| return; | ||
| } | ||
|
|
||
|
|
@@ -298,10 +371,14 @@ | |
| class:position-below={position === "below" || | ||
| (position === "auto" && _autoPosition === "below")} | ||
| class:position-right={position === "right"} | ||
| class:use-anchor-based-positioning={!_needsManualPositioning} | ||
| style={styles( | ||
| style("width", position !== "right" ? width : undefined), | ||
| style("min-width", minwidth), | ||
| style("max-width", position !== "right" && width ? `max(${width}, ${maxwidth})` : maxwidth), | ||
| style( | ||
| "max-width", | ||
| position !== "right" && width ? `max(${width}, ${maxwidth})` : maxwidth, | ||
| ), | ||
| style("padding", _padded ? "var(--goa-space-m)" : "0"), | ||
| )} | ||
| > | ||
|
|
@@ -355,7 +432,6 @@ | |
| filter: var(--goa-popover-shadow, none); | ||
| border: var(--goa-popover-border, none); | ||
| margin: 0; | ||
|
|
||
| position-anchor: --goa-popover-target; | ||
| inset-block-start: anchor(bottom); | ||
| inset-inline-start: anchor(left); | ||
|
|
@@ -364,7 +440,11 @@ | |
| translate: var(--popover-translate-x) var(--popover-translate-y); | ||
| } | ||
|
|
||
| .popover-content.position-above { | ||
| .popover-content.use-anchor-based-positioning { | ||
| inset-block-start: anchor(top); | ||
| --popover-translate-y: calc(-100% - var(--offset-bottom, 3px)); | ||
| } | ||
| .popover-content.use-anchor-based-positioning.position-above { | ||
| inset-block-start: anchor(top); | ||
| --popover-translate-y: calc(-100% - var(--offset-bottom, 3px)); | ||
| position-try-fallbacks: none; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Safari 18 will ignore Anchor Positioning, but it won't ignore all the
translatecalls I put in here. So when you're manually calculating the positioning here, thetranslatecalls are still running. This means when someone is usingbelow, the horizontal and vertical offset will be doubled. And when someone is usingabove, the position of the popover will be twice its height above the element.