Skip to content

Commit

Permalink
feat: allow receiving specified dom events for inert elements (#19121)
Browse files Browse the repository at this point in the history
* Increase logging level for events blocked by modal

Closes #18940

* Allow inter, programmatic API for dom event registration

* Documenting

* formatter:format

* refactoring

* Added unit test

* formatter:format

* blind shot to fix the navigation events & typo fix

* added event type to the log message

* typos etc

* formatter format

* extended unit test test

* cleaned imports and a missing word

---------

Co-authored-by: Teppo Kurki <teppo.kurki@vaadin.com>
  • Loading branch information
mstahv and tepi committed Apr 10, 2024
1 parent dfdc289 commit 63f64cc
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 33 deletions.
Expand Up @@ -391,4 +391,14 @@ default public DomListenerRegistration preventDefault() {
return this;
}

/**
* Configures the event listener to bypass the server side security checks
* for modality. Handle with care! Can be ok when transferring data from
* "non-ui" component events through the Element API, like e.g. geolocation
* events.
*
* @return the DomListenerRegistration for further configuration
*/
public DomListenerRegistration allowInert();

}
Expand Up @@ -29,6 +29,9 @@
import java.util.function.Function;
import java.util.stream.Stream;

import org.slf4j.LoggerFactory;

import com.vaadin.flow.component.UI;
import com.vaadin.flow.dom.DebouncePhase;
import com.vaadin.flow.dom.DisabledUpdateMode;
import com.vaadin.flow.dom.DomEvent;
Expand All @@ -39,7 +42,6 @@
import com.vaadin.flow.internal.JsonUtils;
import com.vaadin.flow.internal.StateNode;
import com.vaadin.flow.shared.JsonConstants;

import elemental.json.Json;
import elemental.json.JsonObject;
import elemental.json.JsonValue;
Expand Down Expand Up @@ -114,6 +116,7 @@ private static class DomEventListenerWrapper
private int debounceTimeout = 0;
private EnumSet<DebouncePhase> debouncePhases = NO_TIMEOUT_PHASES;
private List<SerializableRunnable> unregisterHandlers;
private boolean allowInert;

private DomEventListenerWrapper(ElementListenerMap listenerMap,
String type, DomEventListener origin) {
Expand Down Expand Up @@ -265,6 +268,12 @@ private boolean isPropertySynchronized(String propertyName) {
.contains(JsonConstants.SYNCHRONIZE_PROPERTY_TOKEN
+ propertyName);
}

@Override
public DomListenerRegistration allowInert() {
allowInert = true;
return this;
}
}

/**
Expand Down Expand Up @@ -427,7 +436,15 @@ public void fireEvent(DomEvent event) {
if (listeners == null) {
return;
}
boolean isElementEnabled = event.getSource().isEnabled();

final boolean isElementEnabled = event.getSource().isEnabled();

final boolean isNavigationRequest = UI.BrowserNavigateEvent.EVENT_NAME
.equals(event.getType())
|| UI.BrowserLeaveNavigationEvent.EVENT_NAME
.equals(event.getType());
final boolean inert = event.getSource().getNode().isInert();

List<DomEventListenerWrapper> typeListeners = listeners
.get(event.getType());
if (typeListeners == null) {
Expand All @@ -436,6 +453,15 @@ public void fireEvent(DomEvent event) {

List<DomEventListener> listeners = new ArrayList<>();
for (DomEventListenerWrapper wrapper : typeListeners) {
if (!isNavigationRequest && inert && !wrapper.allowInert) {
// drop as inert
LoggerFactory.getLogger(ElementListenerMap.class.getName())
.info("Ignored listener invocation for {} event from "
+ "the client side for an inert {} element",
event.getType(), event.getSource().getTag());
continue;
}

if ((isElementEnabled
|| DisabledUpdateMode.ALWAYS.equals(wrapper.mode))
&& wrapper.matchesFilter(event.getEventData())
Expand Down
Expand Up @@ -19,18 +19,15 @@
import java.util.List;
import java.util.Optional;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.vaadin.flow.component.PollEvent;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.UI.BrowserLeaveNavigationEvent;
import com.vaadin.flow.component.UI.BrowserNavigateEvent;
import com.vaadin.flow.internal.StateNode;
import com.vaadin.flow.shared.JsonConstants;

import elemental.json.JsonObject;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Abstract invocation handler implementation with common methods.
* <p>
Expand Down Expand Up @@ -62,12 +59,12 @@ public Optional<Runnable> handle(UI ui, JsonObject invocationJson) {
// ignore RPC requests from the client side for the nodes that are
// invisible, disabled or inert
if (node.isInactive()) {
getLogger().trace("Ignored RPC for invocation handler '{}' from "
getLogger().info("Ignored RPC for invocation handler '{}' from "
+ "the client side for an inactive (disabled or invisible) node id='{}'",
getClass().getName(), node.getId());
return Optional.empty();
} else if (!allowInert(ui, invocationJson) && node.isInert()) {
getLogger().trace(
getLogger().info(
"Ignored RPC for invocation handler '{}' from "
+ "the client side for an inert node id='{}'",
getClass().getName(), node.getId());
Expand Down Expand Up @@ -116,22 +113,6 @@ private boolean isPollEventInvocation(JsonObject invocationJson) {
invocationJson.getString(JsonConstants.RPC_EVENT_TYPE));
}

private boolean isNavigationInvocation(JsonObject invocationJson) {
if (!invocationJson.hasKey(JsonConstants.RPC_EVENT_TYPE)) {
return false;
}
if (BrowserNavigateEvent.EVENT_NAME.equals(
invocationJson.getString(JsonConstants.RPC_EVENT_TYPE))) {
return true;
}
if (BrowserLeaveNavigationEvent.EVENT_NAME.equals(
invocationJson.getString(JsonConstants.RPC_EVENT_TYPE))) {
return true;
}
return false;

}

private boolean isPollingEnabledForUI(UI ui) {
return ui.getPollInterval() > 0;
}
Expand Down Expand Up @@ -201,8 +182,7 @@ private boolean isLegitimatePollEventInvocation(UI ui,
* the current invocation or not.
*/
protected boolean allowInert(UI ui, JsonObject invocationJson) {
return isValidPollInvocation(ui, invocationJson)
|| isNavigationInvocation(invocationJson);
return isValidPollInvocation(ui, invocationJson);
}

/**
Expand Down
Expand Up @@ -17,12 +17,12 @@

import java.util.Optional;

import com.vaadin.flow.component.UI;
import com.vaadin.flow.dom.DomEvent;
import com.vaadin.flow.dom.Element;
import com.vaadin.flow.internal.StateNode;
import com.vaadin.flow.internal.nodefeature.ElementListenerMap;
import com.vaadin.flow.shared.JsonConstants;

import elemental.json.Json;
import elemental.json.JsonObject;

Expand Down Expand Up @@ -64,4 +64,9 @@ public Optional<Runnable> handleNode(StateNode node,
return Optional.empty();
}

@Override
protected boolean allowInert(UI ui, JsonObject invocationJson) {
// handled separately in ElementListenerMap
return true;
}
}
29 changes: 28 additions & 1 deletion flow-server/src/test/java/com/vaadin/flow/dom/ElementTest.java
Expand Up @@ -50,6 +50,7 @@
import com.vaadin.flow.internal.nodefeature.ElementListenersTest;
import com.vaadin.flow.internal.nodefeature.ElementPropertyMap;
import com.vaadin.flow.internal.nodefeature.ElementStylePropertyMap;
import com.vaadin.flow.internal.nodefeature.InertData;
import com.vaadin.flow.internal.nodefeature.VirtualChildrenList;
import com.vaadin.flow.server.MockVaadinServletService;
import com.vaadin.flow.server.StreamResource;
Expand All @@ -59,7 +60,6 @@
import com.vaadin.tests.util.AlwaysLockedVaadinSession;
import com.vaadin.tests.util.MockUI;
import com.vaadin.tests.util.TestUtil;

import elemental.json.Json;
import elemental.json.JsonArray;
import elemental.json.JsonObject;
Expand Down Expand Up @@ -382,6 +382,33 @@ public void listenerReceivesEvents() {
Assert.assertEquals(1, listenerCalls.get());
}

@Test
public void listenerReceivesEventsWithAllowInert() {
Element e = ElementFactory.createDiv();
// Inert the node, verify events no more passed through
InertData inertData = e.getNode().getFeature(InertData.class);
inertData.setInertSelf(true);
inertData.generateChangesFromEmpty();

AtomicInteger listenerCalls = new AtomicInteger(0);
DomEventListener myListener = event -> listenerCalls.incrementAndGet();

DomListenerRegistration domListenerRegistration = e
.addEventListener("click", myListener);
Assert.assertEquals(0, listenerCalls.get());
e.getNode().getFeature(ElementListenerMap.class)
.fireEvent(new DomEvent(e, "click", Json.createObject()));
// Event should not go through
Assert.assertEquals(0, listenerCalls.get());

// Now should pass inert check and get notified
domListenerRegistration.allowInert();
e.getNode().getFeature(ElementListenerMap.class)
.fireEvent(new DomEvent(e, "click", Json.createObject()));
Assert.assertEquals(1, listenerCalls.get());

}

@Test
public void getPropertyDefaults() {
Element element = ElementFactory.createDiv();
Expand Down
Expand Up @@ -22,10 +22,11 @@

import com.vaadin.flow.component.ComponentTest.TestComponent;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.dom.DomListenerRegistration;
import com.vaadin.flow.dom.Element;
import com.vaadin.flow.internal.StateNode;
import com.vaadin.flow.internal.nodefeature.InertData;
import com.vaadin.flow.shared.JsonConstants;

import elemental.json.Json;
import elemental.json.JsonObject;

Expand Down Expand Up @@ -53,12 +54,27 @@ public void testElementEventData() throws Exception {
ui.add(c);
AtomicInteger invocationData = new AtomicInteger(0);

element.addEventListener("test-event", e -> invocationData
.addAndGet((int) e.getEventData().getNumber("nr")));
DomListenerRegistration domListenerRegistration = element
.addEventListener("test-event", e -> invocationData
.addAndGet((int) e.getEventData().getNumber("nr")));
JsonObject eventData = Json.createObject();
eventData.put("nr", 123);
sendElementEvent(element, ui, "test-event", eventData);
Assert.assertEquals(123, invocationData.get());

// Also verify inert stops the event and allowInert allows to bypass
invocationData.set(0);
eventData.put("nr", 124);
InertData inertData = element.getNode().getFeature(InertData.class);
inertData.setInertSelf(true);
inertData.generateChangesFromEmpty();
sendElementEvent(element, ui, "test-event", eventData);
Assert.assertEquals(0, invocationData.get());
// explicitly allow this event listener even when element is inert
domListenerRegistration.allowInert();
sendElementEvent(element, ui, "test-event", eventData);
Assert.assertEquals(124, invocationData.get());

}

private static JsonObject createElementEventInvocation(Element element,
Expand Down

0 comments on commit 63f64cc

Please sign in to comment.