diff --git a/src/observers/form_submit_observer.ts b/src/observers/form_submit_observer.ts index 5ffd960ba..3c31fe7ce 100644 --- a/src/observers/form_submit_observer.ts +++ b/src/observers/form_submit_observer.ts @@ -1,3 +1,5 @@ +import { getAttribute } from "../util" + export interface FormSubmitObserverDelegate { willSubmitForm(form: HTMLFormElement, submitter?: HTMLElement): boolean formSubmitted(form: HTMLFormElement, submitter?: HTMLElement): void @@ -41,6 +43,7 @@ export class FormSubmitObserver { form && submissionDoesNotDismissDialog(form, submitter) && submissionDoesNotTargetIFrame(form, submitter) && + submissionDoesNotIntegrateWithUJS(form, submitter) && this.delegate.willSubmitForm(form, submitter) ) { event.preventDefault() @@ -65,3 +68,10 @@ function submissionDoesNotTargetIFrame(form: HTMLFormElement, submitter?: HTMLEl return true } + +function submissionDoesNotIntegrateWithUJS(form: HTMLFormElement, submitter?: HTMLElement): boolean { + const value = getAttribute("data-remote", submitter, form) + const remote = /true/i.test(value || "") + + return !remote +} diff --git a/src/observers/link_click_observer.ts b/src/observers/link_click_observer.ts index 218eb2ba1..a01969bbc 100644 --- a/src/observers/link_click_observer.ts +++ b/src/observers/link_click_observer.ts @@ -38,7 +38,7 @@ export class LinkClickObserver { if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) { const target = (event.composedPath && event.composedPath()[0]) || event.target const link = this.findLinkFromClickTarget(target) - if (link && doesNotTargetIFrame(link)) { + if (link && doesNotTargetIFrame(link) && doesNotIntegrateWithUJS(link)) { const location = this.getLocationForLink(link) if (this.delegate.willFollowLinkToLocation(link, location, event)) { event.preventDefault() @@ -78,3 +78,10 @@ function doesNotTargetIFrame(anchor: HTMLAnchorElement): boolean { return true } + +function doesNotIntegrateWithUJS(anchor: HTMLAnchorElement): boolean { + const value = anchor.getAttribute("data-remote") + const remote = /true/i.test(value || "") + + return !remote +} diff --git a/src/tests/fixtures/ujs.html b/src/tests/fixtures/ujs.html new file mode 100644 index 000000000..3696d0fa4 --- /dev/null +++ b/src/tests/fixtures/ujs.html @@ -0,0 +1,24 @@ + + + + + Frame + + + + + + +

Frames: #frame

+ + navigate #frame to /src/tests/fixtures/frames/frame.html +
+ +
+
+ + diff --git a/src/tests/functional/ujs_tests.ts b/src/tests/functional/ujs_tests.ts new file mode 100644 index 000000000..9188e6cbf --- /dev/null +++ b/src/tests/functional/ujs_tests.ts @@ -0,0 +1,33 @@ +import { Page, test } from "@playwright/test" +import { assert } from "chai" +import { noNextEventOnTarget } from "../helpers/page" + +test.beforeEach(async ({ page }) => { + await page.goto("/src/tests/fixtures/ujs.html") +}) + +test("test clicking a [data-remote=true] anchor within a turbo-frame", async ({ page }) => { + await assertRequestLimit(page, 1, async () => { + await page.click("#frame a[data-remote=true]") + await noNextEventOnTarget(page, "frame", "turbo:frame-load") + + assert.equal(await page.textContent("#frame h2"), "Frames: #frame", "does not navigate the target frame") + }) +}) + +test("test submitting a [data-remote=true] form within a turbo-frame", async ({ page }) => { + await assertRequestLimit(page, 1, async () => { + await page.click("#frame form[data-remote=true] button") + await noNextEventOnTarget(page, "frame", "turbo:frame-load") + + assert.equal(await page.textContent("#frame h2"), "Frames: #frame", "does not navigate the target frame") + }) +}) + +async function assertRequestLimit(page: Page, count: number, callback: () => Promise) { + let requestsStarted = 0 + await page.on("request", () => requestsStarted++) + await callback() + + assert.equal(requestsStarted, count, `only submits ${count} requests`) +}