Skip to content

Commit

Permalink
Ensure Turbo does not interfere with IFrames
Browse files Browse the repository at this point in the history
Skip intercepting `<a>` element clicks or `<form>` element submissions
when they target an `<iframe>`. This occurs when an `<a>` declares a
[target][a-target], when a `<form>` declares a [target][form-target], or
a `<form>` element's submitting `<button>` element declares a
[formtarget][formtarget].

[a-target]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-target
[form-target]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-target
[formtarget]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-formtarget
  • Loading branch information
seanpdoyle committed Jul 16, 2022
1 parent 6eb2cde commit b958895
Show file tree
Hide file tree
Showing 6 changed files with 71 additions and 9 deletions.
31 changes: 24 additions & 7 deletions src/observers/form_submit_observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,31 @@ export class FormSubmitObserver {
const form = event.target instanceof HTMLFormElement ? event.target : undefined
const submitter = event.submitter || undefined

if (form) {
const method = submitter?.getAttribute("formmethod") || form.getAttribute("method")

if (method != "dialog" && this.delegate.willSubmitForm(form, submitter)) {
event.preventDefault()
this.delegate.formSubmitted(form, submitter)
}
if (
form &&
submissionDoesNotDismissDialog(form, submitter) &&
submissionDoesNotTargetIFrame(form, submitter) &&
this.delegate.willSubmitForm(form, submitter)
) {
event.preventDefault()
this.delegate.formSubmitted(form, submitter)
}
}
})
}

function submissionDoesNotDismissDialog(form: HTMLFormElement, submitter?: HTMLElement): boolean {
const method = submitter?.getAttribute("formmethod") || form.getAttribute("method")

return method != "dialog"
}

function submissionDoesNotTargetIFrame(form: HTMLFormElement, submitter?: HTMLElement): boolean {
const target = submitter?.getAttribute("formtarget") || form.target

for (const element of document.getElementsByName(target)) {
if (element instanceof HTMLIFrameElement) return false
}

return true
}
12 changes: 10 additions & 2 deletions src/observers/link_click_observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class LinkClickObserver {
if (this.clickEventIsSignificant(event)) {
const target = (event.composedPath && event.composedPath()[0]) || event.target
const link = this.findLinkFromClickTarget(target)
if (link) {
if (link && doesNotTargetIFrame(link)) {
const location = this.getLocationForLink(link)
if (this.delegate.willFollowLinkToLocation(link, location, event)) {
event.preventDefault()
Expand All @@ -60,11 +60,19 @@ export class LinkClickObserver {

findLinkFromClickTarget(target: EventTarget | null) {
if (target instanceof Element) {
return target.closest("a[href]:not([target^=_]):not([download])")
return target.closest<HTMLAnchorElement>("a[href]:not([target^=_]):not([download])")
}
}

getLocationForLink(link: Element): URL {
return expandURL(link.getAttribute("href") || "")
}
}

function doesNotTargetIFrame(anchor: HTMLAnchorElement): boolean {
for (const element of document.getElementsByName(anchor.target)) {
if (element instanceof HTMLIFrameElement) return false
}

return true
}
11 changes: 11 additions & 0 deletions src/tests/fixtures/form.html
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ <h1>Form</h1>
</form>
</dialog>

<hr>
<dialog id="dialog-method-turbo-frame" open>
<form action="/__turbo/redirect" method="dialog">
<input type="hidden" name="path" value="/src/tests/fixtures/frames/frame.html">
Expand All @@ -196,6 +197,15 @@ <h1>Form</h1>
<button formmethod="dialog">Close</button>
</form>
</dialog>

<hr>
<form action="/src/tests/fixtures/one.html" method="get" target="iframe">
<button>Submit iframe target</button>
</form>

<form action="/src/tests/fixtures/one.html" method="get">
<button formtarget="iframe">Submit iframe formtarget</button>
</form>
</div>
<hr>
<div id="targets-frame">
Expand Down Expand Up @@ -315,5 +325,6 @@ <h2>Frame: Form</h2>
<form method="post" action="https://httpbin.org/post" data-turbo-frame="ignored">
<button id="submit-external-target-ignored">POST to https://httpbin.org/post targeting #hello</button>
</form>
<iframe name="iframe"></iframe>
</body>
</html>
3 changes: 3 additions & 0 deletions src/tests/fixtures/navigation.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,11 @@ <h1>Navigation</h1>
<p><a id="headers-link" href="/__turbo/headers">Headers link</a></p>
<p><custom-link-element id="custom-link-element" link="/src/tests/fixtures/one.html" text="Same-origin unannotated custom element link"></custom-link-element></p>
<p><a id="delayed-link" href="/__turbo/delayed_response">Delayed link</a></p>
<p><a id="targets-iframe" href="/src/tests/fixtures/one.html" target="iframe">Targets iframe</a></p>
</section>

<turbo-frame id="hello" disabled></turbo-frame>

<iframe name="iframe"></iframe>
</body>
</html>
16 changes: 16 additions & 0 deletions src/tests/functional/form_submission_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,22 @@ test("test POST to external action targetting frame ignored", async ({ page }) =
assert.equal(page.url(), "https://httpbin.org/post")
})

test("test form submission skipped with form[target]", async ({ page }) => {
await page.click("#skipped form[target] button")
await nextBeat()

assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html")
assert.notOk(await formSubmitEnded(page))
})

test("test form submission skipped with submitter button[formtarget]", async ({ page }) => {
await page.click("#skipped [formtarget]")
await nextBeat()

assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html")
assert.notOk(await formSubmitEnded(page))
})

function formSubmitStarted(page: Page) {
return getFromLocalStorage(page, "formSubmitStarted")
}
Expand Down
7 changes: 7 additions & 0 deletions src/tests/functional/navigation_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,3 +358,10 @@ test("test navigating back whilst a visit is in-flight", async ({ page }) => {
assert.equal(pathname(page.url()), "/src/tests/fixtures/navigation.html")
assert.equal(await visitAction(page), "restore")
})

test("test ignores links that target an iframe", async ({ page }) => {
await page.click("#targets-iframe")
await nextBeat()

assert.equal(pathname(page.url()), "/src/tests/fixtures/navigation.html")
})

0 comments on commit b958895

Please sign in to comment.