Skip to content

Commit

Permalink
Add better support for shadow DOM (hotwired#758)
Browse files Browse the repository at this point in the history
  • Loading branch information
yuki24 authored Dec 24, 2022
1 parent b8ecc46 commit 2f0d46f
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 9 deletions.
6 changes: 3 additions & 3 deletions src/core/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { StreamMessage } from "./streams/stream_message"
import { StreamMessageRenderer } from "./streams/stream_message_renderer"
import { StreamObserver } from "../observers/stream_observer"
import { Action, Position, StreamSource } from "./types"
import { clearBusyState, dispatch, getVisitAction, markAsBusy } from "../util"
import { clearBusyState, dispatch, findClosestRecursively, getVisitAction, markAsBusy } from "../util"
import { PageView, PageViewDelegate, PageViewRenderOptions } from "./drive/page_view"
import { Visit, VisitOptions } from "./drive/visit"
import { PageSnapshot } from "./drive/page_snapshot"
Expand Down Expand Up @@ -403,8 +403,8 @@ export class Session
}

elementIsNavigatable(element: Element): boolean {
const container = element.closest("[data-turbo]")
const withinFrame = element.closest("turbo-frame")
const container = findClosestRecursively(element, "[data-turbo]")
const withinFrame = findClosestRecursively(element, "turbo-frame")

// Check if Drive is enabled on the session or we're within a Frame.
if (this.drive || withinFrame) {
Expand Down
7 changes: 3 additions & 4 deletions src/observers/link_click_observer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expandURL } from "../core/url"
import { findClosestRecursively } from "../util"

export interface LinkClickObserverDelegate {
willFollowLinkToLocation(link: Element, location: URL, event: MouseEvent): boolean
Expand Down Expand Up @@ -60,10 +61,8 @@ export class LinkClickObserver {
)
}

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

getLocationForLink(link: Element): URL {
Expand Down
3 changes: 3 additions & 0 deletions src/tests/fixtures/drive_disabled.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ <h2>Hello from a frame</h2>
<form action="/src/tests/fixtures/drive_disabled.html">
<button>Navigate #frame</button>
</form>
<custom-link-element link="/src/tests/fixtures/frames/frame.html">
<span id="frame-navigation-with-slot">Link in slot</span>
</custom-link-element>
</turbo-frame>
</body>
</html>
27 changes: 27 additions & 0 deletions src/tests/fixtures/navigation.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,33 @@ <h1>Navigation</h1>
<p><a id="redirection-link" href="/__turbo/redirect?path=/src/tests/fixtures/one.html">Redirection link</a></p>
<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 href="/src/tests/fixtures/one.html">
<custom-button id="shadow-dom-drive-enabled"></custom-button>
</a>
</p>
<p data-turbo="false">
<a href="/src/tests/fixtures/one.html">
<custom-button id="shadow-dom-drive-disabled"></custom-button>
</a>
</p>
<p>
<custom-link-element link="/src/tests/fixtures/one.html">
<span id="element-in-slot">Link in slot</span>
</custom-link-element>
</p>
<p data-turbo="false">
<custom-link-element link="/src/tests/fixtures/one.html">
<span id="element-in-slot-disabled">Link in slot (disabled)</span>
</custom-link-element>
</p>
<p>
<turbo-toggle turbo="false">
<custom-link-element link="/src/tests/fixtures/one.html">
<span id="element-in-nested-slot-disabled">Link in slot (disabled)</span>
</custom-link-element>
</turbo-toggle>
</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>
<p><a id="redirect-to-cache-observer" href="/__turbo/redirect?path=/src/tests/fixtures/cache_observer.html">Redirect to cache_observer.html</a></p>
Expand Down
29 changes: 27 additions & 2 deletions src/tests/fixtures/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,41 @@
"turbo:reload"
])

window.customElements.define('custom-link-element', class extends HTMLElement {
customElements.define('custom-link-element', class extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
}
connectedCallback() {
this.shadowRoot.innerHTML = `
<a href="${this.getAttribute('link')}">
${this.getAttribute('text')}
${this.getAttribute('text') || `<slot></slot>`}
</a>
`
}
})

customElements.define('custom-button', class extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' }).innerHTML = `
<span>
Drive in Shadow DOM
</span>
`
}
})

customElements.define('turbo-toggle', class extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
}
connectedCallback() {
this.shadowRoot.innerHTML = `
<div data-turbo="${this.getAttribute('turbo') || 'true'}">
<slot></slot>
</div>
`
}
})
5 changes: 5 additions & 0 deletions src/tests/functional/drive_disabled_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,8 @@ test("test drive disabled by default; forms within <turbo-frame> navigate with T
await page.click("#frame button")
await nextEventOnTarget(page, "frame", "turbo:frame-render")
})

test("test drive disabled by default; slot within <turbo-frame> navigate with Turbo", async ({ page }) => {
await page.click("#frame-navigation-with-slot")
await nextEventOnTarget(page, "frame", "turbo:frame-render")
})
35 changes: 35 additions & 0 deletions src/tests/functional/navigation_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,41 @@ test("test following a same-origin unannotated custom element link", async ({ pa
assert.equal(await visitAction(page), "advance")
})

test("test drive enabled; click an element in the shadow DOM wrapped by a link in the light DOM", async ({ page }) => {
page.click("#shadow-dom-drive-enabled span")
await nextBody(page)
assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html")
assert.equal(await visitAction(page), "advance")
})

test("test drive disabled; click an element in the shadow DOM within data-turbo='false'", async ({ page }) => {
page.click("#shadow-dom-drive-disabled span")
await nextBody(page)
assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html")
assert.equal(await visitAction(page), "load")
})

test("test drive enabled; click an element in the slot", async ({ page }) => {
page.click("#element-in-slot")
await nextBody(page)
assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html")
assert.equal(await visitAction(page), "advance")
})

test("test drive disabled; click an element in the slot within data-turbo='false'", async ({ page }) => {
page.click("#element-in-slot-disabled")
await nextBody(page)
assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html")
assert.equal(await visitAction(page), "load")
})

test("test drive disabled; click an element in the nested slot within data-turbo='false'", async ({ page }) => {
page.click("#element-in-nested-slot-disabled")
await nextBody(page)
assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html")
assert.equal(await visitAction(page), "load")
})

test("test following a same-origin unannotated link with search params", async ({ page }) => {
page.click("#same-origin-unannotated-link-search-params")
await nextBody(page)
Expand Down
9 changes: 9 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,12 @@ export function setMetaContent(name: string, content: string) {

return element
}

export function findClosestRecursively<E extends Element>(element: Element | null, selector: string): E | undefined {
if (element instanceof Element) {
return (
element.closest<E>(selector) ||
findClosestRecursively(element.assignedSlot || (element.getRootNode() as ShadowRoot)?.host, selector)
)
}
}

0 comments on commit 2f0d46f

Please sign in to comment.