diff --git a/src/core/drive/visit.ts b/src/core/drive/visit.ts index 23811eec8..99d3a597e 100644 --- a/src/core/drive/visit.ts +++ b/src/core/drive/visit.ts @@ -2,7 +2,7 @@ import { Adapter } from "../native/adapter" import { FetchMethod, FetchRequest, FetchRequestDelegate } from "../../http/fetch_request" import { FetchResponse } from "../../http/fetch_response" import { History } from "./history" -import { getAnchor } from "../url" +import { getAnchor, getRequestURL } from "../url" import { PageSnapshot } from "./page_snapshot" import { Action } from "../types" import { uuid } from "../../util" @@ -39,7 +39,8 @@ export type VisitOptions = { historyChanged: boolean, referrer?: URL, snapshotHTML?: string, - response?: VisitResponse + response?: VisitResponse, + restorationIdentifier?: string, } const defaultOptions: VisitOptions = { @@ -70,6 +71,7 @@ export class Visit implements FetchRequestDelegate { frame?: number historyChanged = false location: URL + isSamePage: boolean redirectedToLocation?: URL request?: FetchRequest response?: VisitResponse @@ -81,6 +83,11 @@ export class Visit implements FetchRequestDelegate { constructor(delegate: VisitDelegate, location: URL, restorationIdentifier: string | undefined, options: Partial = {}) { this.delegate = delegate this.location = location + this.isSamePage = ( + getAnchor(location) != null && + getRequestURL(location) === getRequestURL(this.view.lastRenderedLocation) + ) + this.restorationIdentifier = restorationIdentifier || uuid() const { action, historyChanged, referrer, snapshotHTML, response } = { ...defaultOptions, ...options } @@ -251,6 +258,15 @@ export class Visit implements FetchRequestDelegate { } } + goToSamePageAnchor() { + if (this.isSamePage) { + this.render(async () => { + this.cacheSnapshot() + this.adapter.visitRendered(this) + }) + } + } + // Fetch request delegate requestStarted() { @@ -310,8 +326,10 @@ export class Visit implements FetchRequestDelegate { } scrollToAnchor() { - if (getAnchor(this.location)) { - this.view.scrollToAnchor(getAnchor(this.location)) + const anchor = getAnchor(this.location); + + if (anchor) { + this.view.scrollToAnchor(anchor) return true } } @@ -345,9 +363,13 @@ export class Visit implements FetchRequestDelegate { } shouldIssueRequest() { - return this.action == "restore" - ? !this.hasCachedSnapshot() - : true + if (this.isSamePage) { + return false + } else if (this.action == "restore") { + return !this.hasCachedSnapshot() + } else { + return true + } } cacheSnapshot() { diff --git a/src/core/native/browser_adapter.ts b/src/core/native/browser_adapter.ts index 44c2bee87..cbb7a708b 100644 --- a/src/core/native/browser_adapter.ts +++ b/src/core/native/browser_adapter.ts @@ -21,6 +21,7 @@ export class BrowserAdapter implements Adapter { visitStarted(visit: Visit) { visit.issueRequest() visit.changeHistory() + visit.goToSamePageAnchor() visit.loadCachedSnapshot() } diff --git a/src/core/session.ts b/src/core/session.ts index a00fb4fba..44f16ea01 100644 --- a/src/core/session.ts +++ b/src/core/session.ts @@ -109,9 +109,9 @@ export class Session implements FormSubmitObserverDelegate, HistoryDelegate, Lin // History delegate - historyPoppedToLocationWithRestorationIdentifier(location: URL) { + historyPoppedToLocationWithRestorationIdentifier(location: URL, restorationIdentifier: string) { if (this.enabled) { - this.navigator.proposeVisit(location, { action: "restore", historyChanged: true }) + this.navigator.proposeVisit(location, { action: "restore", historyChanged: true, restorationIdentifier }) } else { this.adapter.pageInvalidated() } diff --git a/src/core/snapshot.ts b/src/core/snapshot.ts index 0bb51ca40..973ed9805 100644 --- a/src/core/snapshot.ts +++ b/src/core/snapshot.ts @@ -9,11 +9,11 @@ export class Snapshot { return [ ...this.element.children ] } - hasAnchor(anchor: string) { + hasAnchor(anchor: string | undefined) { return this.getElementForAnchor(anchor) != null } - getElementForAnchor(anchor: string) { + getElementForAnchor(anchor: string | undefined) { try { return this.element.querySelector(`[id='${anchor}'], a[name='${anchor}']`) } catch { diff --git a/src/core/url.ts b/src/core/url.ts index 8efb81d2e..6c96331ce 100644 --- a/src/core/url.ts +++ b/src/core/url.ts @@ -10,8 +10,6 @@ export function getAnchor(url: URL) { return url.hash.slice(1) } else if (anchorMatch = url.href.match(/#(.*)$/)) { return anchorMatch[1] - } else { - return "" } } @@ -28,13 +26,15 @@ export function isPrefixedBy(baseURL: URL, url: URL) { return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix) } +export function getRequestURL(url: URL) { + const anchor = getAnchor(url) + return anchor != null + ? url.href.slice(0, -(anchor.length + 1)) + : url.href +} + export function toCacheKey(url: URL) { - const anchorLength = url.hash.length - if (anchorLength < 2) { - return url.href - } else { - return url.href.slice(0, -anchorLength) - } + return getRequestURL(url) } export function urlsAreEqual(left: string, right: string) { diff --git a/src/core/view.ts b/src/core/view.ts index 3b5fd46c9..adb1ac8be 100644 --- a/src/core/view.ts +++ b/src/core/view.ts @@ -25,6 +25,7 @@ export abstract class View = Snapshot = Snapshot -
+ Skip Link + + Skipped Content + +

Navigation

Same-origin unannotated link

diff --git a/src/tests/helpers/functional_test_case.ts b/src/tests/helpers/functional_test_case.ts index 6a8f645f2..b478a8fcf 100644 --- a/src/tests/helpers/functional_test_case.ts +++ b/src/tests/helpers/functional_test_case.ts @@ -45,6 +45,10 @@ export class FunctionalTestCase extends InternTestCase { return this.evaluate(element => element.scrollIntoView(), element) } + async pressTab(): Promise { + return this.remote.getActiveElement().then(activeElement => activeElement.type(('\uE004'))) // TAB + } + async outerHTMLForSelector(selector: string): Promise { const element = await this.remote.findByCssSelector(selector) return this.evaluate(element => element.outerHTML, element) @@ -98,6 +102,10 @@ export class FunctionalTestCase extends InternTestCase { return this.evaluate(() => document.body as any) } + async getActiveElement() { + return await this.remote.getActiveElement() + } + get location(): Promise { return this.evaluate(() => location.toString()) }