From 86015434a904ab688bee7a38c1249cc110394263 Mon Sep 17 00:00:00 2001 From: Marco Collovati Date: Wed, 12 Feb 2025 14:05:47 +0100 Subject: [PATCH] fix: add global click handler for navigation When using React router, clicks on anchors are intercepted only if Flow client has been initialized. In Hilla or hybrid application, if a Flow view is never navigated, clicking on such links cause the browser to reload the page instead of navigating to the expected view. This change registers a global click handler that intercepts click events and triggers a router navigation if necessary. The new behavior is aligned with vaadin-router. Fixes #20939 --- .../com/vaadin/flow/server/frontend/Flow.tsx | 3 +- .../com/vaadin/flow/server/frontend/Flow.tsx | 51 +++++++++++++++++-- .../flow/server/frontend/vaadin-react.tsx | 2 + 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/flow-plugins/flow-maven-plugin/src/it/flow-addon/src/main/fake-resources/com/vaadin/flow/server/frontend/Flow.tsx b/flow-plugins/flow-maven-plugin/src/it/flow-addon/src/main/fake-resources/com/vaadin/flow/server/frontend/Flow.tsx index c487360fc57..df7f21dd1f5 100644 --- a/flow-plugins/flow-maven-plugin/src/it/flow-addon/src/main/fake-resources/com/vaadin/flow/server/frontend/Flow.tsx +++ b/flow-plugins/flow-maven-plugin/src/it/flow-addon/src/main/fake-resources/com/vaadin/flow/server/frontend/Flow.tsx @@ -15,4 +15,5 @@ */ // Resource loaded from project dependency -export const serverSideRoutes = [] +export const serverSideRoutes = []; +export const registerGlobalClickHandler = () => {}; diff --git a/flow-server/src/main/resources/com/vaadin/flow/server/frontend/Flow.tsx b/flow-server/src/main/resources/com/vaadin/flow/server/frontend/Flow.tsx index 5ea3fbf0d3d..1893732c9f7 100644 --- a/flow-server/src/main/resources/com/vaadin/flow/server/frontend/Flow.tsx +++ b/flow-server/src/main/resources/com/vaadin/flow/server/frontend/Flow.tsx @@ -29,6 +29,10 @@ const router = { } }; +const flowReact : { active: boolean } = { + active: false, +} + // ClickHandler for vaadin-router-go event is copied from vaadin/router click.js // @ts-ignore function getAnchorOrigin(anchor) { @@ -55,7 +59,7 @@ function normalizeURL(url: URL): void | string { return '/' + url.href.slice(document.baseURI.length); } -function extractPath(event: MouseEvent): void | string { +function extractURL(event: MouseEvent): void | URL { // ignore the click if the default action is prevented if (event.defaultPrevented) { return; @@ -131,9 +135,45 @@ function extractPath(event: MouseEvent): void | string { return; } - return normalizeURL(new URL(anchor.href, anchor.baseURI)); + return new URL(anchor.href, anchor.baseURI); } +function extractPath(event: MouseEvent): void | string { + const url = extractURL(event); + if (!url) { + return; + } + return normalizeURL(url); +} + +export const registerGlobalClickHandler = () => { + window.addEventListener('click', (event: MouseEvent) => { + if (flowReact.active) { + return; + } + const url = extractURL(event); + if (!url) { + return; + } + // ignore click if baseURI does not match the document (external) + if (!url.href.startsWith(document.baseURI)) { + return; + } + if (event && event.preventDefault) { + event.preventDefault(); + } + + // Normalize path against baseURI + const path = url.pathname + url.search + url.hash; + const state = {...window.history.state} + if (state.idx !== undefined) { + state.idx = state.idx + 1; + } + window.history.pushState(state, '', path); + window.dispatchEvent(new PopStateEvent('popstate')); + }, { capture: false }); +}; + /** * Fire 'vaadin-navigated' event to inform components of navigation. * @param pathname pathname of navigation @@ -352,7 +392,6 @@ function Flow() { if (event && event.preventDefault) { event.preventDefault(); } - navigated.current = false; // When navigation is triggered by click on a link, fromAnchor is set to true // in order to get a server round-trip even when navigating to the same URL again @@ -417,10 +456,15 @@ function Flow() { }, [vaadinRouterGoEventHandler, vaadinNavigateEventHandler]); useEffect(() => { + window.addEventListener('click', navigateEventHandler); + flowReact.active = true; + return () => { containerRef.current?.parentNode?.removeChild(containerRef.current); containerRef.current?.removeEventListener('flow-portal-add', addPortalEventHandler as EventListener); containerRef.current = undefined; + window.removeEventListener('click', navigateEventHandler); + flowReact.active = false; }; }, []); @@ -533,7 +577,6 @@ function Flow() { if (outlet && outlet !== container.parentNode) { outlet.append(container); container.addEventListener('flow-portal-add', addPortalEventHandler as EventListener); - window.addEventListener('click', navigateEventHandler); containerRef.current = container; } return container.onBeforeEnter?.call( diff --git a/flow-server/src/main/resources/com/vaadin/flow/server/frontend/vaadin-react.tsx b/flow-server/src/main/resources/com/vaadin/flow/server/frontend/vaadin-react.tsx index 549cdc83384..b51fcc4615f 100644 --- a/flow-server/src/main/resources/com/vaadin/flow/server/frontend/vaadin-react.tsx +++ b/flow-server/src/main/resources/com/vaadin/flow/server/frontend/vaadin-react.tsx @@ -1,7 +1,9 @@ import { routes } from "%routesJsImportPath%"; +import { registerGlobalClickHandler } from "Frontend/generated/flow/Flow.js"; (window as any).Vaadin ??= {}; (window as any).Vaadin.routesConfig = routes; +registerGlobalClickHandler(); export { routes as forHMROnly };