Skip to content

Commit

Permalink
fix: perfect hash scroll (QwikDev#4550)
Browse files Browse the repository at this point in the history
  • Loading branch information
jordanw66 authored Jun 21, 2023
1 parent 192fbd6 commit a70895f
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 6 deletions.
1 change: 1 addition & 0 deletions packages/qwik-city/runtime/src/client-navigate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const clientNavigate = (
if (navType !== 'popstate') {
const samePath = isSamePath(fromURL, toURL);
const sameHash = fromURL.hash === toURL.hash;

if (!samePath || !sameHash) {
const newState = {
_qCityScroll: newScrollState(),
Expand Down
43 changes: 41 additions & 2 deletions packages/qwik-city/runtime/src/qwik-city-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,13 @@ import type {
} from './types';
import { loadClientData } from './use-endpoint';
import { useQwikCityEnv } from './use-functions';
import { isSamePathname, toUrl } from './utils';
import { isSameOrigin, isSamePath, isSamePathname, toUrl } from './utils';
import { clientNavigate } from './client-navigate';
import {
currentScrollState,
getScrollHistory,
saveScrollHistory,
scrollToHashId,
toLastPositionOnPopState,
} from './scroll-restoration';

Expand Down Expand Up @@ -157,8 +158,22 @@ export const QwikCityProvider = component$<QwikCityProps>((props) => {
replaceState = false,
} = typeof opt === 'object' ? opt : { forceReload: opt };
const lastDest = routeInternal.value.dest;
const dest = path === undefined ? lastDest : toUrl(path, routeLocation.url);
let dest = path === undefined ? lastDest : toUrl(path, routeLocation.url);

// Remove empty # before sending them into Navigate, it introduces too many edgecases.
dest = !dest.hash && dest.href.endsWith('#') ? new URL(dest.href.slice(0, -1)) : dest;

if (!forceReload && dest.href === lastDest.href) {
if (isBrowser) {
if (type === 'link') {
if (dest.hash) {
scrollToHashId(dest.hash);
} else {
window.scrollTo(0, 0);
}
}
}

return;
}
routeInternal.value = { type, dest, replaceState };
Expand Down Expand Up @@ -314,6 +329,30 @@ export const QwikCityProvider = component$<QwikCityProps>((props) => {

win.removeEventListener('popstate', win._qCityPopstateFallback!);

// Chromium and WebKit fire popstate+hashchange for all #anchor clicks,
// ... even if the URL is already on the #hash.
// Firefox only does it once and no more, but will still scroll. It also sets state to null.
// Any <a> tags w/ #hash href will break SPA state in Firefox.
// However, Chromium & WebKit also create too many edgecase problems with <a href="#">.
// We patch these events and direct them to Link pipeline during SPA.
document.body.addEventListener('click', (event) => {
if (event.defaultPrevented) {
return;
}

const target = (event.target as HTMLElement).closest('a[href*="#"]');

if (target && !target.getAttribute('preventdefault:click')) {
const prev = routeLocation.url;
const dest = toUrl(target.getAttribute('href')!, prev);
// Patch only same-page hash anchors.
if (isSameOrigin(dest, prev) && isSamePath(dest, prev)) {
event.preventDefault();
goto(target.getAttribute('href')!);
}
}
});

// TODO Remove block after Navigation API PR.
// Calling `history.replaceState` during `visibilitychange` in Chromium will nuke BFCache.
// Only Chromium 96 - 101 have BFCache without Navigation API. (<1% of users)
Expand Down
12 changes: 8 additions & 4 deletions packages/qwik-city/runtime/src/scroll-restoration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ export const toTopAlways: QRL<RestoreScroll> = $((_type, fromUrl, toUrl) => () =
*/
export const toLastPositionOnPopState: QRL<RestoreScroll> = $(
(type, fromUrl, toUrl, scrollState) => () => {
// Chromium & Firefox will always natively restore on popstate, only scroll to hash on regular navigate.
if (type === 'popstate' || !scrollForHashChange(fromUrl, toUrl)) {
// Chromium & Firefox will always natively restore on visited popstates.
// Always scroll to known state if available on pop. Otherwise, try hash scroll.
if ((type === 'popstate' && scrollState) || !scrollForHashChange(fromUrl, toUrl)) {
let [scrollX, scrollY] = [0, 0];
if (type === 'popstate' && scrollState) {
if (scrollState) {
scrollX = scrollState.scrollX;
scrollY = scrollState.scrollY;
}
Expand Down Expand Up @@ -55,7 +56,10 @@ const scrollForHashChange = (fromUrl: URL, toUrl: URL): boolean => {
return true;
};

const scrollToHashId = (hash: string) => {
/**
* @alpha
*/
export const scrollToHashId = (hash: string) => {
const elmId = hash.slice(1);
const elm = document.getElementById(elmId);
if (elm) {
Expand Down

0 comments on commit a70895f

Please sign in to comment.