From beec52c17c77a5518488952e4862e8cb1bd4baa0 Mon Sep 17 00:00:00 2001 From: Danny Asmussen Date: Fri, 15 Aug 2025 01:12:13 +0200 Subject: [PATCH] Ensure the radial menu is within the viewport (#1817) ## Description: This PR ensures the radial menu is within the viewport. When clicking right next to the edge of the screen in the browser, the menu will now use transition to ease itself into the viewport, so the menu will stay visible at all times. Issue: https://github.com/openfrontio/OpenFrontIO/issues/1596 image ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory - [X] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: WoodyDRN --- src/client/graphics/layers/RadialMenu.ts | 39 ++++++++++++++++++++---- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/src/client/graphics/layers/RadialMenu.ts b/src/client/graphics/layers/RadialMenu.ts index 9e81cc7cc..ad40dca6d 100644 --- a/src/client/graphics/layers/RadialMenu.ts +++ b/src/client/graphics/layers/RadialMenu.ts @@ -57,6 +57,9 @@ export class RadialMenu implements Layer { private lastHideTime: number = 0; private reopenCooldownMs: number = 300; + private anchorX = 0; + private anchorY = 0; + private menuGroups: Map< number, d3.Selection @@ -146,6 +149,7 @@ export class RadialMenu implements Layer { .style("position", "absolute") .style("top", "50%") .style("left", "50%") + .style("transition", `top ${this.config.menuTransitionDuration}ms ease, left ${this.config.menuTransitionDuration}ms ease`) .style("transform", "translate(-50%, -50%)") .style("pointer-events", "all") .on("click", (event) => this.hideRadialMenu()); @@ -576,6 +580,7 @@ export class RadialMenu implements Layer { this.currentMenuItems = children; this.currentLevel++; + this.clampAndSetMenuPositionForLevel(this.currentLevel); this.renderMenuItems(this.currentMenuItems, this.currentLevel); this.updateMenuGroupVisibility(); this.animatePreviousMenu(); @@ -655,6 +660,7 @@ export class RadialMenu implements Layer { this.isTransitioning = true; this.updateMenuLevels(); + this.clampAndSetMenuPositionForLevel(this.currentLevel); this.clearSelectedItemHoverState(); this.updateMenuVisibility("backward"); this.animateMenuTransitions(); @@ -751,19 +757,17 @@ export class RadialMenu implements Layer { this.resetMenu(); this.isTransitioning = false; this.selectedItemId = null; + this.anchorX = x; + this.anchorY = y; this.menuElement.style("display", "block"); - - this.menuElement - .select("svg") - .style("top", `${y}px`) - .style("left", `${x}px`) - .style("transform", `translate(-50%, -50%)`); + this.clampAndSetMenuPositionForLevel(this.currentLevel); this.isVisible = true; this.renderMenuItems(this.currentMenuItems, this.currentLevel); this.onCenterButtonHover(true); + window.addEventListener("resize", this.handleResize); } public hideRadialMenu() { @@ -787,6 +791,7 @@ export class RadialMenu implements Layer { this.menuIcons.clear(); this.lastHideTime = Date.now(); + window.removeEventListener("resize", this.handleResize); } private handleCenterButtonClick() { @@ -1038,4 +1043,26 @@ export class RadialMenu implements Layer { this.tooltipElement.style.display = "none"; } } + + // Ensure the menu's SVG center stays within viewport given the current level's outer radius + private clampAndSetMenuPositionForLevel(level: number) { + const outerRadius = this.getOuterRadiusForLevel(level); + const margin = Math.max(outerRadius, this.config.centerButtonSize) + 10; + + const vw = window.innerWidth; + const vh = window.innerHeight; + + // If the menu cannot fully fit on an axis, pin it to the viewport center on that axis. + const clampedX = 2 * margin > vw ? vw / 2 : Math.min(Math.max(this.anchorX, margin), vw - margin); + const clampedY = 2 * margin > vh ? vh / 2 : Math.min(Math.max(this.anchorY, margin), vh - margin); + + const svgSel = this.menuElement.select("svg"); + svgSel + .style("top", `${clampedY}px`) + .style("left", `${clampedX}px`); + } + + private handleResize = () => { + if (this.isVisible) this.clampAndSetMenuPositionForLevel(this.currentLevel); + }; }