diff --git a/src/main/resources/META-INF/resources/styles.css b/src/main/resources/META-INF/resources/styles.css index 745b9fe..b381244 100644 --- a/src/main/resources/META-INF/resources/styles.css +++ b/src/main/resources/META-INF/resources/styles.css @@ -307,13 +307,26 @@ apex-chart.dialect-sparkline .apexcharts-xaxis { its left one slot (32px) further in. */ .dialect-action-menu { position: absolute; - top: 11px; + top: 14px; right: 16px; z-index: 1; opacity: 0; transition: opacity 120ms ease; } +/* Its button is a corner control like the grip and the close button, so it is + sized like one. A menu-bar button at its own size is 64px wide, i.e. twice + its slot: painted after the grip, it swallowed the grip's pointer events and + left the widget undraggable. The button is a light-DOM child of the menu bar, + so plain selectors reach it. */ +.dialect-action-menu vaadin-menu-bar-button { + width: 28px; + min-width: 0; + height: 28px; + padding: 0; + color: var(--dialect-ink); +} + .grid-stack-item:has(> .dialect-close-button) > .dialect-action-menu { right: 48px; } diff --git a/src/test/java/com/example/e2e/WidgetActionMenuPlaywrightTest.java b/src/test/java/com/example/e2e/WidgetActionMenuPlaywrightTest.java index 5f39f1b..8549981 100644 --- a/src/test/java/com/example/e2e/WidgetActionMenuPlaywrightTest.java +++ b/src/test/java/com/example/e2e/WidgetActionMenuPlaywrightTest.java @@ -2,6 +2,7 @@ package com.example.e2e; import com.microsoft.playwright.Download; import com.microsoft.playwright.Locator; +import com.microsoft.playwright.Mouse; import com.microsoft.playwright.options.BoundingBox; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -12,6 +13,7 @@ import java.util.regex.Pattern; import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -43,6 +45,45 @@ class WidgetActionMenuPlaywrightTest extends PlaywrightTestBase { assertEquals(before, slot(item), "the widget must not have moved"); } + /** The other half of it: the grip has to stay reachable next to the menu. + * Both sit in the item's top right corner and the menu is painted after + * the grip, so a menu wider than its corner slot takes the grip's pointer + * events and the widget cannot be dragged at all any more. */ + @Test + void draggingTheGrip_movesTheWidget() { + Locator item = widget(); + String before = slot(item); + + BoundingBox grip = grip(item).boundingBox(); + double x = grip.x + grip.width / 2; + double y = grip.y + grip.height / 2; + assertEquals("dialect-drag-handle", topmostClassAt(x, y), + "another corner control covers the grip"); + + page.mouse().move(x, y); + page.mouse().down(); + // In steps, not one jump: gridstack starts the drag on the first move + // past its threshold and places the item off the moves after that, so a + // single move would only ever arm the drag. + page.mouse().move(x + 240, y + 200, new Mouse.MoveOptions().setSteps(20)); + page.mouse().up(); + + page.waitForCondition(() -> !before.equals(slot(item))); + assertNotEquals(before, slot(item), "the widget must have moved"); + } + + /** The class of the corner control the browser actually hits at that point, + * or the topmost element's own class list if it is no corner control. */ + private String topmostClassAt(double x, double y) { + return (String) page.evaluate(""" + ([x, y]) => { + const el = document.elementFromPoint(x, y); + const control = el?.closest( + '.dialect-drag-handle, .dialect-close-button, .dialect-action-menu'); + return control ? control.className : String(el?.className ?? el); + }""", java.util.List.of(x, y)); + } + @Test void menuButton_opensTheActions() { menuButton(widget()).click(); @@ -111,6 +152,10 @@ class WidgetActionMenuPlaywrightTest extends PlaywrightTestBase { }"""); } + private Locator grip(Locator item) { + return item.locator("> .dialect-drag-handle"); + } + private Locator menuButton(Locator item) { return item.locator(".dialect-action-menu vaadin-menu-bar-button").first(); }