Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ public class ClickEvent<C extends Component> extends ComponentEvent<C> {
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;
Expand Down Expand Up @@ -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
* <code>true</code> if the event was originally fired on the
* client, <code>false</code> 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
* <code>true</code> if the control key was down when the event
* was fired, <code>false</code> otherwise
* @param shiftKey
* <code>true</code> if the shift key was down when the event was
* fired, <code>false</code> otherwise
* @param altKey
* <code>true</code> if the alt key was down when the event was
* fired, <code>false</code> otherwise
* @param metaKey
* <code>true</code> if the meta key was down when the event was
* fired, <code>false</code> 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,
Comment on lines +150 to +151
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep the old constructor that doesn't have these two new coordinates parameters and create a new one with these two parameters. This is for backwards compatibility.

The old constructor can delegate to the new one with giving "-1" values to the new fields.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added backward compatibility by keeping the original constructor without relativeX and relativeY parameters. The old constructor delegates to the new one with -1 values for the relative coordinates, ensuring all existing code continues to work unchanged. Committed in ae3d665.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This perhaps not that easy, because DomEvent and EventData require a special single constructor where the framework injects JS values. I.e. having two constructors would not work. This means we have to revert to original version with one constructor and admit that this is a breaking change.

@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;
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Component> 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<Component> 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<Component> 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<Component> 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());
}
Comment on lines +109 to +127
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get the purpose of this test - it compares semantically different coordinates and no doubt they are different if different values are assigned. Requesting to delete this test.

}
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create a new test class RelativeCoordinatesIT that uses TestBench API for checking that the "output" element has the expected text (coordinates).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created RelativeCoordinatesIT TestBench test class that verifies the output element contains expected coordinate text after clicking the click area. The test validates the coordinate format and ensures relative coordinates are within reasonable bounds. Committed in e1ddc6f.


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<Div> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
Loading