Skip to content

Commit ea2a317

Browse files
authored
prevent full app reload when using Button under Link widget (#1122)
1 parent 6809ae3 commit ea2a317

File tree

4 files changed

+59
-3
lines changed

4 files changed

+59
-3
lines changed

.changeset/strange-years-cry.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@ensembleui/react-kitchen-sink": patch
3+
"@ensembleui/react-runtime": patch
4+
---
5+
6+
prevent full app reload when using Button under Link widget

apps/kitchen-sink/src/ensemble/screens/widgets.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1331,8 +1331,8 @@ View:
13311331
color: blue
13321332
hoverColor: red
13331333
widget:
1334-
Text:
1335-
text: Forms Page (with hover effect)
1334+
Button:
1335+
label: Forms Page (with hover effect)
13361336

13371337
- Card:
13381338
styles:

packages/runtime/src/widgets/Link.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Expression, EnsembleAction } from "@ensembleui/react-framework";
22
import { useRegisterBindings, unwrapWidget } from "@ensembleui/react-framework";
33
import { useMemo, useCallback } from "react";
4-
import { Link as RouterLink } from "react-router-dom";
4+
import { Link as RouterLink, useNavigate } from "react-router-dom";
55
import { cloneDeep } from "lodash-es";
66
import { WidgetRegistry } from "../registry";
77
import type { EnsembleWidgetProps } from "../shared/types";
@@ -41,6 +41,7 @@ export type LinkProps = {
4141

4242
export const Link: React.FC<LinkProps> = ({ id, onTap, widget, ...rest }) => {
4343
const action = useEnsembleAction(onTap);
44+
const navigate = useNavigate();
4445

4546
const { values, rootRef } = useRegisterBindings({ ...rest, widgetName }, id);
4647

@@ -92,6 +93,25 @@ export const Link: React.FC<LinkProps> = ({ id, onTap, widget, ...rest }) => {
9293
);
9394
}, [values?.url]);
9495

96+
// Intercept normal left-clicks (no modifiers) during capture phase
97+
// This ensures navigation is handled client-side even if the child (e.g., Button)
98+
// calls stopPropagation in its onClick. Right/middle clicks remain native.
99+
const onClickCapture = useCallback(
100+
(e: React.MouseEvent) => {
101+
if (!values?.url || isExternalUrl) return;
102+
// only handle left click without modifiers
103+
if (e.button !== 0) return;
104+
if (e.metaKey || e.ctrlKey || e.altKey || e.shiftKey) return;
105+
106+
e.preventDefault();
107+
navigate(values.url, {
108+
replace: Boolean(values.replace),
109+
state: values.inputs,
110+
});
111+
},
112+
[navigate, values?.url, values?.replace, values?.inputs, isExternalUrl],
113+
);
114+
95115
if (!values?.url) {
96116
return (
97117
<span ref={rootRef} style={linkStyles}>
@@ -130,6 +150,7 @@ export const Link: React.FC<LinkProps> = ({ id, onTap, widget, ...rest }) => {
130150
// For internal navigation, use React Router Link
131151
return (
132152
<RouterLink
153+
onClickCapture={onClickCapture}
133154
onClick={handleClick}
134155
onMouseEnter={(e): void => {
135156
if (hoverStyles.color) e.currentTarget.style.color = hoverStyles.color;

packages/runtime/src/widgets/__tests__/Link.test.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,35 @@ describe("Link Widget", () => {
443443

444444
consoleSpy.mockRestore();
445445
});
446+
447+
test("navigates on left-click when Link wraps Button (capture-phase)", async () => {
448+
let currentLocation: ReturnType<typeof useLocation>;
449+
450+
const LocationTracker = (): null => {
451+
const location = useLocation();
452+
currentLocation = location;
453+
return null;
454+
};
455+
456+
render(
457+
<MemoryRouter initialEntries={["/one/foo"]}>
458+
<LocationTracker />
459+
<Link
460+
url="/two/abc"
461+
widget={{
462+
Button: { label: "Open Link" },
463+
}}
464+
/>
465+
</MemoryRouter>,
466+
);
467+
468+
const buttonText = screen.getByText("Open Link");
469+
fireEvent.click(buttonText, { button: 0 });
470+
471+
await waitFor(() => {
472+
expect(currentLocation.pathname).toBe("/two/abc");
473+
});
474+
});
446475
});
447476

448477
describe("Link Widget Integration Tests", () => {

0 commit comments

Comments
 (0)