diff --git a/flow-server/src/main/java/com/vaadin/flow/component/ClickEvent.java b/flow-server/src/main/java/com/vaadin/flow/component/ClickEvent.java index 0fedc40415f..39333d8e013 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/ClickEvent.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/ClickEvent.java @@ -33,6 +33,9 @@ public class ClickEvent extends ComponentEvent { private final int clientX; private final int clientY; + private final int relativeX; + private final int relativeY; + private final int clickCount; private final int button; private final boolean ctrlKey; @@ -90,11 +93,75 @@ public ClickEvent(Component source, boolean fromClient, @EventData("event.shiftKey") boolean shiftKey, @EventData("event.altKey") boolean altKey, @EventData("event.metaKey") boolean metaKey) { + this(source, fromClient, screenX, screenY, clientX, clientY, -1, -1, + clickCount, button, ctrlKey, shiftKey, altKey, metaKey); + } + + /** + * Creates a new click event with relative coordinates. + * + * @param source + * the component that fired the event + * @param fromClient + * true if the event was originally fired on the + * client, false if the event originates from + * server-side logic + * @param screenX + * the x coordinate of the click event, relative to the upper + * left corner of the screen, -1 if unknown + * @param screenY + * the y coordinate of the click event, relative to the upper + * left corner of the screen, -i if unknown + * @param clientX + * the x coordinate of the click event, relative to the upper + * left corner of the browser viewport, -1 if unknown + * @param clientY + * the y coordinate of the click event, relative to the upper + * left corner of the browser viewport, -1 if unknown + * @param relativeX + * the x coordinate of the click event, relative to the upper + * left corner of the clicked component, -1 if unknown + * @param relativeY + * the y coordinate of the click event, relative to the upper + * left corner of the clicked component, -1 if unknown + * @param clickCount + * the number of consecutive clicks recently recorded + * @param button + * the id of the pressed mouse button + * @param ctrlKey + * true if the control key was down when the event + * was fired, false otherwise + * @param shiftKey + * true if the shift key was down when the event was + * fired, false otherwise + * @param altKey + * true if the alt key was down when the event was + * fired, false otherwise + * @param metaKey + * true if the meta key was down when the event was + * fired, false otherwise + * + */ + public ClickEvent(Component source, boolean fromClient, + @EventData("event.screenX") int screenX, + @EventData("event.screenY") int screenY, + @EventData("event.clientX") int clientX, + @EventData("event.clientY") int clientY, + @EventData("event.clientX - element.getBoundingClientRect().left") int relativeX, + @EventData("event.clientY - element.getBoundingClientRect().top") int relativeY, + @EventData("event.detail") int clickCount, + @EventData("event.button") int button, + @EventData("event.ctrlKey") boolean ctrlKey, + @EventData("event.shiftKey") boolean shiftKey, + @EventData("event.altKey") boolean altKey, + @EventData("event.metaKey") boolean metaKey) { super((C) source, fromClient); this.screenX = screenX; this.screenY = screenY; this.clientX = clientX; this.clientY = clientY; + this.relativeX = relativeX; + this.relativeY = relativeY; this.clickCount = clickCount; this.button = button; this.ctrlKey = ctrlKey; @@ -110,8 +177,8 @@ public ClickEvent(Component source, boolean fromClient, * the component that fired the event */ public ClickEvent(Component source) { - // source, notClient, 4 coordinates, clickCount, button, 4 modifier keys - this(source, false, -1, -1, -1, -1, 1, -1, false, false, false, false); + // source, notClient, 4 coordinates, relative coordinates, clickCount, button, 4 modifier keys + this(source, false, -1, -1, -1, -1, -1, -1, 1, -1, false, false, false, false); } /** @@ -154,6 +221,26 @@ public int getScreenY() { return screenY; } + /** + * Gets the x coordinate of the click event, relative to the upper left + * corner of the clicked component. + * + * @return the x coordinate, -1 if unknown + */ + public int getRelativeX() { + return relativeX; + } + + /** + * Gets the y coordinate of the click event, relative to the upper left + * corner of the clicked component. + * + * @return the y coordinate, -1 if unknown + */ + public int getRelativeY() { + return relativeY; + } + /** * Gets the number of consecutive clicks recently recorded. * diff --git a/flow-server/src/test/java/com/vaadin/flow/component/ClickEventTest.java b/flow-server/src/test/java/com/vaadin/flow/component/ClickEventTest.java new file mode 100644 index 00000000000..c48df8829c4 --- /dev/null +++ b/flow-server/src/test/java/com/vaadin/flow/component/ClickEventTest.java @@ -0,0 +1,128 @@ +/* + * Copyright 2000-2025 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component; + +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.flow.dom.Element; + +public class ClickEventTest { + + @Test + public void serverSideConstructor() { + Component component = new Component(new Element("div")) {}; + ClickEvent event = new ClickEvent<>(component); + + Assert.assertEquals(component, event.getSource()); + Assert.assertFalse(event.isFromClient()); + + // All coordinates should be -1 for server-side events + Assert.assertEquals(-1, event.getScreenX()); + Assert.assertEquals(-1, event.getScreenY()); + Assert.assertEquals(-1, event.getClientX()); + Assert.assertEquals(-1, event.getClientY()); + Assert.assertEquals(-1, event.getRelativeX()); + Assert.assertEquals(-1, event.getRelativeY()); + + Assert.assertEquals(1, event.getClickCount()); + Assert.assertEquals(-1, event.getButton()); + Assert.assertFalse(event.isCtrlKey()); + Assert.assertFalse(event.isShiftKey()); + Assert.assertFalse(event.isAltKey()); + Assert.assertFalse(event.isMetaKey()); + } + + @Test + public void clientSideConstructor() { + Component component = new Component(new Element("div")) {}; + ClickEvent event = new ClickEvent<>(component, true, + 100, 200, // screen coordinates + 150, 250, // client coordinates + 10, 20, // relative coordinates + 2, // click count + 0, // button (left mouse button) + true, false, true, false); // modifier keys + + Assert.assertEquals(component, event.getSource()); + Assert.assertTrue(event.isFromClient()); + + Assert.assertEquals(100, event.getScreenX()); + Assert.assertEquals(200, event.getScreenY()); + Assert.assertEquals(150, event.getClientX()); + Assert.assertEquals(250, event.getClientY()); + Assert.assertEquals(10, event.getRelativeX()); + Assert.assertEquals(20, event.getRelativeY()); + + Assert.assertEquals(2, event.getClickCount()); + Assert.assertEquals(0, event.getButton()); + Assert.assertTrue(event.isCtrlKey()); + Assert.assertFalse(event.isShiftKey()); + Assert.assertTrue(event.isAltKey()); + Assert.assertFalse(event.isMetaKey()); + } + + @Test + public void oldClientSideConstructorBackwardCompatibility() { + Component component = new Component(new Element("div")) {}; + // Test the old constructor without relative coordinates + ClickEvent event = new ClickEvent<>(component, true, + 100, 200, // screen coordinates + 150, 250, // client coordinates + 2, // click count + 0, // button (left mouse button) + true, false, true, false); // modifier keys + + Assert.assertEquals(component, event.getSource()); + Assert.assertTrue(event.isFromClient()); + + Assert.assertEquals(100, event.getScreenX()); + Assert.assertEquals(200, event.getScreenY()); + Assert.assertEquals(150, event.getClientX()); + Assert.assertEquals(250, event.getClientY()); + + // Relative coordinates should be -1 when using old constructor + Assert.assertEquals(-1, event.getRelativeX()); + Assert.assertEquals(-1, event.getRelativeY()); + + Assert.assertEquals(2, event.getClickCount()); + Assert.assertEquals(0, event.getButton()); + Assert.assertTrue(event.isCtrlKey()); + Assert.assertFalse(event.isShiftKey()); + Assert.assertTrue(event.isAltKey()); + Assert.assertFalse(event.isMetaKey()); + } + + @Test + public void relativeCoordinatesAreDistinctFromOtherCoordinates() { + Component component = new Component(new Element("div")) {}; + ClickEvent event = new ClickEvent<>(component, true, + 100, 200, // screen coordinates + 150, 250, // client coordinates + 10, 20, // relative coordinates (different from screen/client) + 1, 0, false, false, false, false); + + // Verify that relative coordinates are different from screen and client coordinates + Assert.assertNotEquals(event.getScreenX(), event.getRelativeX()); + Assert.assertNotEquals(event.getScreenY(), event.getRelativeY()); + Assert.assertNotEquals(event.getClientX(), event.getRelativeX()); + Assert.assertNotEquals(event.getClientY(), event.getRelativeY()); + + // Verify the actual values + Assert.assertEquals(10, event.getRelativeX()); + Assert.assertEquals(20, event.getRelativeY()); + } +} \ No newline at end of file diff --git a/flow-tests/test-root-context/src/main/java/com/vaadin/flow/uitest/ui/RelativeCoordinatesView.java b/flow-tests/test-root-context/src/main/java/com/vaadin/flow/uitest/ui/RelativeCoordinatesView.java new file mode 100644 index 00000000000..3ecaf2c0b6b --- /dev/null +++ b/flow-tests/test-root-context/src/main/java/com/vaadin/flow/uitest/ui/RelativeCoordinatesView.java @@ -0,0 +1,70 @@ +/* + * Copyright 2000-2025 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.flow.uitest.ui; + +import com.vaadin.flow.component.ClickEvent; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.uitest.servlet.ViewTestLayout; + +@Route(value = "com.vaadin.flow.uitest.ui.RelativeCoordinatesView", layout = ViewTestLayout.class) +public class RelativeCoordinatesView extends Div { + + public static final String CLICK_AREA_ID = "click-area"; + public static final String OUTPUT_ID = "output"; + + public RelativeCoordinatesView() { + Div clickArea = new Div(); + clickArea.setId(CLICK_AREA_ID); + clickArea.setText("Click anywhere on this area to see relative coordinates"); + clickArea.getStyle() + .set("background-color", "#f0f0f0") + .set("border", "2px solid #ccc") + .set("padding", "50px") + .set("margin", "20px") + .set("width", "400px") + .set("height", "200px") + .set("cursor", "pointer"); + + Span output = new Span(); + output.setId(OUTPUT_ID); + output.setText("Click on the area above to see coordinates"); + + clickArea.addClickListener(this::handleClick); + + add(clickArea, output); + } + + private void handleClick(ClickEvent
event) { + String coordinates = String.format( + "Screen: (%d, %d), Client: (%d, %d), Relative: (%d, %d)", + event.getScreenX(), event.getScreenY(), + event.getClientX(), event.getClientY(), + event.getRelativeX(), event.getRelativeY() + ); + + Span output = (Span) getChildren() + .filter(component -> OUTPUT_ID.equals(component.getId().orElse(""))) + .findFirst() + .orElse(null); + + if (output != null) { + output.setText(coordinates); + } + } +} \ No newline at end of file diff --git a/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/RelativeCoordinatesIT.java b/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/RelativeCoordinatesIT.java new file mode 100644 index 00000000000..a8da1f6b18f --- /dev/null +++ b/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/RelativeCoordinatesIT.java @@ -0,0 +1,67 @@ +/* + * Copyright 2000-2025 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.uitest.ui; + +import org.junit.Assert; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +import com.vaadin.flow.testutil.ChromeBrowserTest; + +public class RelativeCoordinatesIT extends ChromeBrowserTest { + + @Test + public void clickShowsRelativeCoordinates() { + open(); + + WebElement clickArea = findElement(By.id(RelativeCoordinatesView.CLICK_AREA_ID)); + WebElement output = findElement(By.id(RelativeCoordinatesView.OUTPUT_ID)); + + // Verify initial state + Assert.assertEquals("Click on the area above to see coordinates", output.getText()); + + // Click on the click area + clickArea.click(); + + // Verify output contains expected coordinate format + String outputText = output.getText(); + Assert.assertTrue("Output should contain coordinate information", + outputText.contains("Screen:") && outputText.contains("Client:") && outputText.contains("Relative:")); + + // Verify the format matches the expected pattern + String expectedPattern = "Screen: \\(\\d+, \\d+\\), Client: \\(\\d+, \\d+\\), Relative: \\(\\d+, \\d+\\)"; + Assert.assertTrue("Output should match coordinate pattern: " + outputText, + outputText.matches(expectedPattern)); + + // Verify relative coordinates are reasonable (non-negative and within bounds) + String[] parts = outputText.split(", Relative: \\("); + if (parts.length >= 2) { + String relativePart = parts[1].replace(")", ""); + String[] coords = relativePart.split(", "); + if (coords.length == 2) { + int relativeX = Integer.parseInt(coords[0]); + int relativeY = Integer.parseInt(coords[1]); + + Assert.assertTrue("Relative X should be non-negative", relativeX >= 0); + Assert.assertTrue("Relative Y should be non-negative", relativeY >= 0); + // The click area has width 400px and height 200px + padding, so coordinates should be reasonable + Assert.assertTrue("Relative X should be within reasonable bounds", relativeX < 600); + Assert.assertTrue("Relative Y should be within reasonable bounds", relativeY < 400); + } + } + } +} \ No newline at end of file